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

Contents

Documentación de ASP.NET Core


Información general
Acerca de ASP.NET Core
Comparación de ASP.NET Core y ASP.NET
Comparación de .NET Core y .NET Framework
Primeros pasos
Novedades
Novedades de la versión 2.2
Novedades de la versión 2.1
Novedades de la versión 2.0
Novedades de la versión 1.1
Tutoriales
Aplicaciones web
Páginas de Razor
Información general
Primeros pasos
Adición de un modelo
Scaffolding
Trabajar con una base de datos
Actualización de las páginas
Adición de búsqueda
Agregar un campo nuevo
Agregar validación
MVC
Información general
Primeros pasos
Incorporación de un controlador
Agregar una vista
Adición de un modelo
Trabajar con una base de datos
Acciones y vistas del controlador
Adición de búsqueda
Agregar un campo nuevo
Agregar validación
Examinar los métodos Details y Delete
Blazor
Aplicaciones de API web
Creación de una API web
API web con MongoDB
Back-end para dispositivos móviles
Aplicaciones web en tiempo real
SignalR con JavaScript
SignalR con TypeScript
Aplicaciones de llamada a procedimiento remoto
Introducción al servicio gRPC
Acceso a datos
EF Core con Razor Pages
Información general
Primeros pasos
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
EF Core con MVC, base de datos existente
EF Core con MVC, base de datos nueva
EF Core con MVC, 10 tutoriales
Información general
Primeros pasos
Creación, lectura, actualización y eliminación
Ordenación, filtrado, paginación y agrupación
Migraciones
Creación de un modelo de datos complejo
Lectura de datos relacionados
Actualización de datos relacionados
Control de conflictos de simultaneidad
Herencia
Temas avanzados
Tutoriales (Microsoft Learn)
Aplicaciones de API web
Acceso a datos
Aspectos básicos
Información general
Clase Startup
Inserción de dependencias (servicios)
Software intermedio
administrador de flujos de trabajo
Host genérico
Host web
Servidores
Configuración
Opciones
Entornos (desarrollo, preparación, producción)
Registro
Enrutamiento
Control de errores
Realización de solicitudes HTTP
Archivos estáticos
Generador de código
Aplicaciones web
Páginas de Razor
Introducción
Tutorial
Información general
Primeros pasos
Adición de un modelo
Scaffolding
Trabajar con una base de datos
Actualización de las páginas
Adición de búsqueda
Agregar un campo nuevo
Agregar validación
Filtros
Convenciones de rutas y aplicaciones
Carga de archivos
SDK de Razor
MVC
Información general
Tutorial
Información general
Primeros pasos
Incorporación de un controlador
Agregar una vista
Adición de un modelo
Trabajar con una base de datos
Acciones y vistas del controlador
Adición de búsqueda
Agregar un campo nuevo
Agregar validación
Examinar los métodos Details y Delete
Vistas
Vistas parciales
Controladores
Enrutamiento
Cargas de archivos
Inserción de dependencias: controladores
Inserción de dependencias: vistas
Prueba unitaria
Blazor
Información general
Plataformas compatibles
Primeros pasos
Modelos de hospedaje
Compilación de la primera aplicación
Componentes
Formularios y validación
Bibliotecas de los componentes
Diseños
Inserción de dependencias
Enrutamiento
Interoperabilidad de JavaScript
Seguridad e identidad
Depuración
Llamada a una API de web
Hospedaje e implementación
Información general
Lado cliente
Lado servidor
Configurar el enlazador
Desarrollo del lado del cliente
Aplicaciones de una sola página
Angular
React
React con Redux
Servicios de JavaScript
LibMan
Información general
CLI
Programa para la mejora
Grunt
Bower
Agrupar y minimizar
Vínculo con exploradores
Estado de sesión y aplicación
Diseño
Sintaxis de Razor
Bibliotecas de clases de Razor
Asistentes de etiquetas
Información general
Creación de asistentes de etiquetas
Uso de asistentes de etiquetas en formularios
Componentes de asistente de etiquetas
Asistentes de etiquetas integradas
Delimitador
instancias y claves
Caché distribuida
Entorno
Form
Imagen
Entrada
Etiqueta
Parcial
Seleccionar
Textarea
Mensaje de validación
Resumen de validación
Avanzadas
Componentes de vista
Visualización de compilación
Modelo de aplicación
Filtros
Áreas
Elementos de la aplicación
Aplicaciones de API web
Información general
Tutoriales
Creación de una API web
API web con MongoDB
Swagger / OpenAPI
Información general
Introducción a Swashbuckle
Introducción a NSwag
Tipos de valor devueltos de acción
Aplicación de formato a datos de respuesta
Formateadores personalizados
Analizadores
Convenciones
Aplicaciones en tiempo real
Introducción a SignalR
Plataformas compatibles
Tutoriales
SignalR con JavaScript
SignalR con TypeScript
Muestras
Conceptos de servidor
Concentradores
Envío desde fuera de un concentrador
Usuarios y grupos
Consideraciones de diseño de API
Clientes
Cliente .NET
Referencia de API de .NET
Cliente de Java
Referencia de API de Java
Cliente de JavaScript
Referencia de API de JavaScript
Hospedaje y ajuste de la escala
Información general
Azure App Service
Backplane de Redis
SignalR con servicios en segundo plano
Configuración
Autenticación y autorización
Consideraciones de seguridad
Protocolo de concentrador MessagePack
Streaming
Comparación de SignalR y SignalR Core
WebSockets sin SignalR
Registro y diagnósticos
Aplicaciones de llamada a procedimiento remoto
Introducción a los servicios gRPC
Servicios gRPC con C#
Servicios gRPC con ASP.NET Core
Configuración
Migración de servicios gRPC de CCore a ASP.NET Core
Comparación entre los servicios gRPC y las API HTTP
Probar, depurar y solucionar problemas
Pruebas unitarias
Pruebas unitarias de Razor Pages
Controladores de pruebas
Depuración remota
Depuración de instantáneas
Depuración de instantáneas en Visual Studio
Pruebas de integración
Pruebas de esfuerzo y carga
Solucionar problemas
Registro
Acceso a datos
Tutoriales
EF Core con Razor Pages
Información general
Primeros pasos
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
EF Core con MVC, base de datos nueva
EF Core con MVC, base de datos existente
EF Core con MVC, 10 tutoriales
Información general
Primeros pasos
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
EF 6 con ASP.NET Core
Azure Storage con Visual Studio
Servicios conectados
Blob Storage
Queue Storage
Table Storage
Hospedaje e implementación
Información general
Hospedaje en Azure App Service
Información general
Publicación con Visual Studio
Publicar con Visual Studio para Mac
Publicación con herramientas de la CLI
Publicación con Visual Studio y Git
Implementación continua con Azure Pipelines
Módulo ASP.NET Core
Solucionar problemas
Referencia de errores
DevOps
Información general
Herramientas y descargas
Implementación en App Service
Integración e implementación continuas
Supervisión y solución de problemas
Pasos siguientes
Hospedaje en Windows con IIS
Información general
Módulo ASP.NET Core
Compatibilidad con IIS en Visual Studio
Módulos de IIS
Solucionar problemas
Referencia de errores
Transformación de web.config
Kestrel
HTTP.sys
Hospedaje en un servicio de Windows
Hospedaje en Linux con Nginx
Hospedaje en Linux con Apache
Hospedaje en Docker
Información general
Creación de imágenes de Docker
Visual Studio Tools
Publicación en una imagen de Docker
Imágenes de Docker de muestra
Configuración del proxy y del equilibrador de carga
Hospedaje en una granja de servidores web
Perfiles de publicación de Visual Studio
Publicar en carpeta de Visual Studio para Mac
Estructura de directorios
Comprobaciones de estado
Blazor
Información general
Lado cliente
Lado servidor
Configurar el enlazador
Seguridad e identidad
Información general
Autenticación
Introducción a Identity
Identidad con SPA
Identidad de scaffolding
Agregar datos de usuario personalizados a Identity
Ejemplos de autenticación
Personalizar Identity
Opciones de autenticación de OSS de la comunidad
Configuración de Identity
Configuración de la autenticación de Windows
Proveedores de almacenamiento personalizados para Identity
Google, Facebook...
Información general
Autenticación con Google
Autenticación con Facebook
Autenticación con Microsoft
Autenticación con Twitter
Otros proveedores
Notificaciones adicionales
Esquemas de directivas
Autenticación con WS-Federation
Confirmación de cuentas y recuperación de contraseñas
Habilitar la generación de código QR en Identity
Autenticación en dos fases con SMS
Uso de la autenticación de cookies sin Identity
Uso de la autenticación social sin Identity
Azure Active Directory
Información general
Integración de Azure AD en una aplicación web
Integración de AAD B2C en una aplicación web
Integración de Azure AD B2C en una API web
Llamada a una API web desde WPF
Llamada a una API web en una aplicación web con Azure AD
Protección de aplicaciones de ASP.NET Core con IdentityServer4
Protección de aplicaciones de ASP.NET Core con la autenticación de Azure App
Service (autenticación sencilla)
Cuentas de usuario individuales
Configuración de la autenticación de los certificados
Autorización
Información general
Creación de una aplicación web con autorización
Convenciones de autorización de Razor Pages
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
Limitación de la identidad por esquema
Protección de datos
Información general
API de protección de datos
API de consumidor
Información general
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
Información general
Configuración de la protección de datos
Configuración predeterminada
Directiva de todo el equipo
Escenarios no compatibles con DI
API de extensibilidad
Información general
Extensibilidad de criptografía de núcleo
Extensibilidad de administración de claves
Otras API
Implementación
Información general
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
Información general
Sustitución de machineKey en ASP.NET
Administración de secretos
Protección de secretos en el desarrollo
Proveedor de configuración de Azure Key Vault
Aplicación de HTTPS
Compatibilidad con el Reglamento general de protección de datos (GDPR) de la UE
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
Seguridad de aplicaciones: OWASP
Blazor
Rendimiento
Información general
Almacenamiento en caché de respuestas
Información general
Caché en memoria
Almacenamiento en caché distribuido
Middleware de almacenamiento en caché de respuestas
Compresión de las respuestas
Herramientas de diagnóstico
Pruebas de esfuerzo y carga
Globalización y localización
Información general
Localización de un objeto portátil
Solucionar problemas
Avanzadas
Enlace de modelos
Enlace de modelos personalizado
Validación de modelos
Versión de compatibilidad
Escritura de software intermedio
Operaciones de solicitud y respuesta
Reescritura de direcciones URL
Proveedores de archivos
Interfaces de solicitud de características
Acceso a HttpContext
Cambio de tokens
Interfaz web abierta para .NET (OWIN)
Tareas en segundo plano con servicios hospedados
Ensamblados de inicio de hospedaje
Metapaquete Microsoft.AspNetCore
Metapaquete Microsoft.AspNetCore.All
Registro con LoggerMessage
Uso de un monitor de archivos
Middleware basado en Factory
Middleware basado en Factory con un contenedor de terceros
Migración
2.2 a 3.0
2.1 a 2.2
2.0 a 2.1
1.x a 2.0
Información general
Autenticación e identidad
De ASP.NET a ASP.NET Core
Información general
MVC
Web API
Configuración
Autenticación e identidad
ClaimsPrincipal.Current
De pertenencia a identidad
De módulos HTTP a middleware
Registro (no en ASP.NET Core)
referencia de API
Contribuir
Introducción a ASP.NET Core
02/07/2019 • 11 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é elegir 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.
Diseñado para la capacidad de prueba.
Razor Pages hace que la codificación de escenarios centrados en páginas sean más sencillos y productivos.
Capacidad para desarrollarse y ejecutarse en Windows, macOS y Linux.
De código abierto y centrado en la comunidad.
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.

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.
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.
El marcado de Razor proporciona una sintaxis productiva para las páginas de Razor y las vistas de MVC.
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.
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 del cliente


ASP.NET Core se integra perfectamente con bibliotecas y marcos populares del lado cliente, que incluyen Blazor,
Angular, React y Bootstrap. Para más información, consulte Introducción a Blazor en ASP.NET Core y los temas
relacionados en Client-side development (Desarrollo del lado cliente).

ASP.NET Core con .NET Framework como destino


ASP.NET Core 2.x 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. Por lo
general, ASP.NET Core 2.x está formado por bibliotecas de .NET Standard. Las bibliotecas escritas con .NET
Standard 2.0 se ejecutan en cualquier plataforma .NET que implementa .NET Standard 2.0.
ASP.NET Core 2.x se admite en las versiones de .NET Framework que implementan .NET Standard 2.0:
Se recomienda la versión más reciente de .NET Framework.
.NET Framework 4.6.1 y posterior.
ASP.NET Core 3.0 y versiones posteriores solo se ejecutan en .NET Core. Para obtener más información sobre
este cambio, vea A first look at changes coming in ASP.NET Core 3.0 (Descripción general de los cambios que se
aplicarán a ASP.NET Core 3.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.
Código Abierto
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.

Ruta de aprendizaje recomendada


Se recomienda la siguiente secuencia de tutoriales y artículos para obtener una introducción para desarrollar
aplicaciones de ASP.NET Core:
1. Siga un tutorial para el tipo de aplicación que quiere desarrollar o mantener:

TIPO DE APLICACIÓN ESCENARIO TUTORIAL

Aplicación web Para un nuevo desarrollo Introducción a las páginas de Razor

Aplicación web Para mantener una aplicación MVC Introducción a MVC

Web API Creación de una API web*

Aplicación en tiempo real Introducción a SignalR


2. Siga un tutorial que muestra cómo realizar el acceso a datos básicos:

ESCENARIO TUTORIAL

Para un nuevo desarrollo Razor Pages con Entity Framework Core

Para mantener una aplicación MVC MVC con Entity Framework Core

3. Lea una introducción a las características de ASP.NET Core que se aplican a todos los tipos de
aplicaciones:
Aspectos básicos
4. Examine la tabla de contenido para ver otros temas de interés.
* Hay un nuevo tutorial de API web que sigue completamente en el explorador, no es necesaria una instalación
del IDE local. El código se ejecuta en un Azure Cloud Shell y se usa curl para realizar pruebas.

Cómo descargar un ejemplo


En muchos de los artículos y tutoriales se incluyen vínculos a código de 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.
Directivas de preprocesador en código de ejemplo
Para mostrar varios escenarios, las aplicaciones de ejemplo usan las instrucciones #define y
#if-#else/#elif-#endif de C# para compilar de forma selectiva y ejecutar secciones distintas de código de
ejemplo. Para los ejemplos que usan este enfoque, establezca la instrucción #define en la parte superior de los
archivos C# con el símbolo asociado con el escenario que quiera ejecutar. Algunos ejemplos requieren establecer
el símbolo en la parte superior de varios archivos para ejecutar un escenario.
Por ejemplo, la siguiente lista de símbolos de #define indica que hay cuatro escenarios disponibles (un
escenario por símbolo). La configuración de ejemplo actual ejecuta el escenario TemplateCode :

#define TemplateCode // or LogFromMain or ExpandDefault or FilterInCode

Para cambiar el ejemplo el escenario ExpandDefault , defina el símbolo ExpandDefault y deje los símbolos
restantes comentados:

#define ExpandDefault // TemplateCode or LogFromMain or FilterInCode

Para obtener información sobre cómo usar directivas de preprocesador de C# para compilar selectivamente
secciones de código, vea #define (Referencia de C#) e #if (Referencia de C#).
Regiones en código de ejemplo
Algunas aplicaciones de ejemplo contienen secciones de código rodeadas de las instrucciones #region y #end-
region de C#. El sistema de creación de documentación inserta estas regiones en los temas de documentación
representados.
Normalmente, los nombres de región contienen la palabra "snippet". En el ejemplo siguiente se muestra una
región denominada snippet_FilterInCode :
#region snippet_FilterInCode
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace))
.Build();
#endregion

En el archivo Markdown del tema se hace referencia al fragmento de código de C# anterior con la siguiente línea:

[!code-csharp[](sample/SampleApp/Program.cs?name=snippet_FilterInCode)]

Puede ignorar sin problemas (o incluso quitar) las instrucciones #region y #endregion que rodean el código. No
altere el código de estas instrucciones y tiene planeado ejecutar los escenarios de ejemplo descritos en el tema.
Puede alterarlo si quiere experimentar con otros escenarios.
Para obtener más información, consulte Contribute to the ASP.NET documentation: Code snippets (Contribución
a la documentación de ASP.NET: fragmentos de código).

Pasos siguientes
Para obtener más información, vea los siguientes recursos:
Introducción a ASP.NET Core
Publicar una aplicación de ASP.NET Core en Azure con Visual Studio
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.
Elección entre ASP.NET 4.x y ASP.NET Core
02/07/2019 • 3 minutes to read • Edit Online

ASP.NET Core es un rediseño de ASP.NET 4.x. En este artículo se enumeran las diferencias entre ellos.

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 Core ofrece las siguientes ventajas:
Un caso unificado para crear API web y una interfaz de usuario web.
Diseñado para la capacidad de prueba.
Razor Pages hace que la codificación de escenarios centrados en páginas sean más sencillos y productivos.
Capacidad para desarrollarse y ejecutarse en Windows, macOS y Linux.
De código abierto y centrado en la comunidad.
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.

ASP.NET 4.x
ASP.NET 4.x es un marco consolidado que proporciona los servicios necesarios para compilar aplicaciones web de
nivel empresarial basadas en servidor en Windows.

Selección del marco


En la tabla siguiente se compara ASP.NET Core en ASP.NET 4.x.

ASP.NET CORE ASP.NET 4.X

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 4.x Buen rendimiento


ASP.NET CORE ASP.NET 4.X

Elegir .NET Framework o .NET Core Usar el tiempo de ejecución de .NET Framework

Vea ASP.NET Core con .NET Framework como destino para obtener información sobre la compatibilidad de
ASP.NET Core 2.x en .NET Framework.

Escenarios de ASP.NET Core


Sitios web
API
En tiempo real
Implementación de una aplicación ASP.NET Core en Azure

Escenarios de ASP.NET 4.x


Sitios web
API
En tiempo real
Creación de una aplicación web ASP.NET 4.x en Azure

Recursos adicionales
Introducción a ASP.NET
Introducción a ASP.NET Core
Implementar aplicaciones de ASP.NET Core en Azure App Service
Tutorial: Introducción a ASP.NET Core
16/05/2019 • 3 minutes to read • Edit Online

En este tutorial se muestra cómo usar la interfaz de la línea de comandos de .NET Core para crear y ejecutar una
aplicación web ASP.NET Core.
Aprenderá a:
Crear un proyecto de aplicación web.
Confíe en el certificado de desarrollo.
Ejecutar la aplicación.
Editar una página de Razor.
Al final, tendrá una aplicación web en funcionamiento ejecutándose en el equipo local.

Requisitos previos
SDK de .NET Core 2.2

Crear un proyecto de aplicación web


Abra un shell de comandos y escriba el siguiente comando:

dotnet new webapp -o aspnetcoreapp

Confíe en el certificado de desarrollo


Confíe en el certificado de desarrollo HTTPS:
Windows
macOS
Linux

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


Para más información, consulte Confiar en el certificado de desarrollo de ASP.NET Core HTTPS

Ejecutar la aplicación
Ejecute los comandos siguientes:

cd aspnetcoreapp
dotnet run

Después de que el shell de comandos indique que se ha iniciado la aplicación, vaya a https://localhost:5001. Haga
clic en Aceptar para aceptar la política de privacidad y de cookies. Esta aplicación no conserva información de
carácter personal.

Editar una página de Razor


Abra Pages/Index.cshtml y modifique la página con el siguiente marcado resaltado:

@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Hello, world! The time on the server is @DateTime.Now</p>
</div>

Vaya a https://localhost:5001 y confirme que los cambios aparecen reflejados.

Pasos siguientes
En este tutorial ha aprendido a:
Crear un proyecto de aplicación web.
Confíe en el certificado de desarrollo.
Ejecute el proyecto.
Realizar un cambio.
Para obtener más información sobre ASP.NET Core, vea la ruta de aprendizaje recomendada en la introducción:
Introducción a ASP.NET Core
Novedades de ASP.NET Core 2.2
02/07/2019 • 11 minutes to read • Edit Online

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

Convenciones y analizadores de OpenAPI


OpenAPI (antes conocido como Swagger) es una especificación independiente del lenguaje que sirve para
describir API REST. El ecosistema de OpenAPI dispone de herramientas que permiten descubrir, probar y generar
código de cliente mediante la especificación. El soporte técnico para generar y visualizar los documentos de
OpenAPI en ASP.NET Core MVC se proporciona a través de proyectos controlados por la comunidad como
NSwag y Swashbuckle.AspNetCore. ASP.NET Core 2.2 proporciona experiencias de uso de herramientas y
entornos de ejecución mejoradas para crear documentos de OpenAPI.
Para obtener más información, vea los siguientes recursos:
Uso de analizadores de API web
Uso de convenciones de API web
ASP.NET Core 2.2.0-preview1: Convenciones y analizadores de OpenAPI

Soporte técnico para los detalles del problema


ASP.NET Core 2.1 introdujo ProblemDetails , según la especificación RFC 7807, para comunicar los detalles de un
error con una respuesta HTTP. En 2.2, ProblemDetails es la respuesta estándar para los códigos de error de cliente
en los controladores con el atributo ApiControllerAttribute . Un elemento IActionResult que anteriormente
devolvía un código de estado de error de cliente (4xx) ahora devuelve un cuerpo ProblemDetails . El resultado
también incluye un identificador de correlación que se puede usar para correlacionar el error mediante los
registros de solicitudes. En el caso de los errores de cliente, el procedimiento predeterminado de
ProducesResponseType es utilizar ProblemDetails como tipo de respuesta. Esto se documenta en los resultados de
OpenAPI/Swagger que se generan mediante NSwag o Swashbuckle.AspNetCore.

Enrutamiento de punto de conexión


ASP.NET Core 2.2 usa un nuevo sistema de enrutamiento de punto de conexión para mejorar la distribución de las
solicitudes. Los cambios incluyen nuevos miembros de API de generación de vínculo y transformadores de
parámetro de ruta.
Para obtener más información, vea los siguientes recursos:
Enrutamiento de punto de conexión en la versión 2.2
Transformadores de parámetro de ruta (consultar la sección Enrutamiento)
Diferencias entre el enrutamiento basado en IRouter y en el punto de conexión

Comprobaciones de estado
Un nuevo servicio de comprobaciones de estado facilita el uso de ASP.NET Core en entornos que requieren
comprobaciones de estado, como Kubernetes. Las comprobaciones de estado incluyen middleware y un conjunto
de bibliotecas que definen una abstracción y un servicio de IHealthCheck .
Un orquestador de contenedores o un equilibrador de carga utilizan las comprobaciones de estado para
determinar rápidamente si un sistema está respondiendo correctamente a las solicitudes. Para responder a una
comprobación de estado con errores, es posible que un orquestador de contenedores detenga una implementación
en curso o reinicie un contenedor. Para responder a una comprobación de estado, es posible que un equilibrador de
carga enrute el tráfico al margen de la instancia con errores del servicio.
Una aplicación expone las comprobaciones de estado como un punto de conexión HTTP que los sistemas de
supervisión utilizan. Las comprobaciones de estado pueden configurarse para diversos escenarios y sistemas de
supervisión en tiempo real. El servicio de comprobaciones de estado se integra con el proyecto BeatPulse, lo que
facilita agregar comprobaciones de docenas de sistemas y dependencias conocidos.
Para obtener más información, consulte Comprobaciones de estado en ASP.NET Core.

HTTP/2 en Kestrel
ASP.NET Core 2.2 es compatible con HTTP/2.
HTTP/2 es una revisión completa del protocolo HTTP. Entre las características más importantes de HTTP/2
destacan la compresión de encabezados y las secuencias totalmente multiplexadas a través de una sola conexión.
Aunque HTTP/2 conserva la semántica de HTTP (encabezados y métodos HTTP, etc.), la manera de entramar y
enviar estos datos es una diferencia importante respecto a HTTP/1.x.
Como consecuencia de este cambio en las tramas, los servidores y los clientes deben negociar la versión del
protocolo que se va a utilizar. La negociación de protocolo de capa de aplicación (ALPN ) es una extensión TLS que
permite que el servidor y el cliente negocien la versión del protocolo que se va a utilizar como parte de su
protocolo de enlace TLS. Aunque es posible que el servidor y el cliente conozcan previamente el protocolo, los
principales exploradores admiten ALPN como la única forma de establecer una conexión HTTP/2.
Para obtener más información, consulte Compatibilidad con HTTP/2.

Configuración de Kestrel
En versiones anteriores de ASP.NET Core, las opciones de Kestrel se configuran mediante una llamada a
UseKestrel . En la versión 2.2, las opciones de Kestrel se configuran mediante una llamada a ConfigureKestrel en
el generador de host. Este cambio resuelve un problema con el orden de los registros de IServer para el
hospedaje en proceso. Para obtener más información, vea los siguientes recursos:
Mitigación de conflictos de UseIIS
Configuración de las opciones del servidor Kestrel con ConfigureKestrel

Hospedaje en proceso de IIS


En versiones anteriores de ASP.NET Core, IIS actuaba como un proxy inverso. En la versión 2.2, el módulo
ASP.NET Core puede arrancar el CoreCLR y hospedar una aplicación dentro del proceso de trabajo de IIS
(w3wp.exe). El hospedaje en proceso proporciona mejoras de rendimiento y diagnóstico cuando se ejecuta con IIS.
Para obtener más información, consulte Modelo de hospedaje en proceso.

Cliente de SignalR Java


ASP.NET Core 2.2 presenta un nuevo cliente de Java para SignalR. Este cliente admite la conexión a un servidor de
SignalR de ASP.NET Core desde código de Java, incluidas las aplicaciones Android.
Para obtener más información, consulte Cliente de Java para SignalR de ASP.NET Core.

Mejoras de CORS
En versiones anteriores de ASP.NET Core, CORS Middleware permitía que los encabezados Accept ,
Accept-Language , Content-Language y Origin se enviaran independientemente de los valores configurados en
CorsPolicy.Headers . En la versión 2.2, cumplir la directiva de CORS Middleware solo es posible cuando los
encabezados enviados en Access-Control-Request-Headers coinciden exactamente con los indicados en
WithHeaders .

Para obtener más información consulte CORS Middleware.

Compresión de las respuestas


ASP.NET Core 2.2 puede comprimir las respuestas con el formato de compresión Brotli.
Para obtener más información, consulte Compresión de respuesta en ASP.NET Core.

Plantillas de proyecto
Las plantillas de proyecto web de ASP.NET Core se han actualizado a Bootstrap 4 y Angular 6. La nueva apariencia
es más sencilla y permite ver con más facilidad las estructuras importantes de la aplicación.

Rendimiento de la validación
El sistema de validación de MVC está diseñado para ser extensible y flexible, lo que permite determinar en función
de la solicitud qué validadores se aplican a un modelo determinado. Esto es muy útil para crear proveedores de
validación compleja. Sin embargo, por lo general, una aplicación solo usa los validadores integrados y no requiere
esta flexibilidad adicional. Los validadores integrados incluyen DataAnnotations como [Required], [StringLength] y
IValidatableObject .

En ASP.NET Core 2.2, MVC puede cortocircuitar la validación si determina que un gráfico de modelo determinado
no requiere validación. Al validar modelos que no pueden tener o no tienen validadores, se producen mejoras
significativas si se omite la validación. Esto incluye objetos como colecciones de primitivos (como byte[] ,
string[] o Dictionary<string, string> ) o gráficos de objetos complejos sin muchos validadores.

Rendimiento del cliente HTTP


En ASP.NET Core 2.2, para mejorar el rendimiento de SocketsHttpHandler , se ha reducido la contención del
bloqueo de grupo de conexiones. Se ha mejorado el rendimiento para las aplicaciones que realizan muchas
solicitudes HTTP salientes, por ejemplo, algunas arquitecturas de microservicios. Bajo una carga, el rendimiento de
HttpClient se puede mejorar hasta en un 60 % en Linux y en un 20 % en Windows.
Para obtener más información, consulte la solicitud de incorporación de cambios que propició esta mejora.

Información adicional
Para ver la lista completa de cambios, consulte las Notas de la versión de ASP.NET Core 2.2.
Novedades de ASP.NET Core 2.1
10/05/2019 • 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, consulte Kestrel web server
implementation: Endpoint configuration (Kestrel: configuración de los puntos 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, consulte Kestrel web server implementation:
Transport configuration (Implementación del servidor web de 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 obtener más información, consulte:
Uso de la plantilla de proyecto de Angular con ASP.NET Core
Uso de la plantilla de proyecto de React con ASP.NET Core
Uso de la plantilla de proyecto React-with-Redux 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
03/07/2019 • 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.
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 obtener información acerca de cómo
crear una SPA en ASP.NET Core, vea Usar servicios de JavaScript para crear aplicaciones de página única en
ASP.NET Core.

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
17/06/2019 • 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 asistentes 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.
Tutorial: Creación de una aplicación web de páginas
de Razor con ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

En esta serie de tutoriales se explican los conceptos básicos de creación de una aplicación web de Razor Pages.
Para acceder a una introducción más avanzada pensada para desarrolladores con experiencia, consulte
Introducción a Razor Pages.
Esta serie incluye los siguientes tutoriales:
1. Creación de una aplicación web de 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 una base de datos
5. Actualización de páginas
6. Agregar búsqueda
7. Agregar un campo nuevo
8. Agregar validación
Al final, conseguirá una aplicación que puede mostrar y administrar una base de datos de películas.

Recursos adicionales
Versión en YouTube de este tutorial
Tutorial: Introducción a Razor Pages en ASP.NET Core
04/07/2019 • 11 minutes to read • Edit Online

Por Rick Anderson


Este es el primer tutorial de una serie. En la serie se enseñan los conceptos básicos de la compilación de una
aplicación web de Razor Pages en ASP.NET Core.
Para acceder a una introducción más avanzada pensada para desarrolladores con experiencia, consulte
Introducción a Razor Pages.
Al final de la serie, tendrá una aplicación que puede administrar una base de datos de películas.
Vea o descargue el código de ejemplo (cómo descargarlo).
En este tutorial ha:
Crear una aplicación web de Razor Pages.
Ejecutar la aplicación.
Examinar los archivos de proyecto.
Al final de este tutorial, tendrá una aplicación web de Razor Pages que compilará en los tutoriales posteriores.

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.

Creación de una aplicación web de páginas de Razor


Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Cree una nueva aplicación web de ASP.NET Core y seleccione Siguiente.

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.2 en la lista desplegable, después Aplicación web y, por último, Crear.

Se crea el proyecto de inicio siguiente:


Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:

Haga clic en Sí si confía en el certificado SSL de IIS Express.


Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener más información, vea Confiar en el certificado de desarrollo de ASP.NET Core HTTPS .
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 página principal de la aplicación, seleccione Aceptar para dar su consentimiento al seguimiento.
Esta aplicación no realiza un seguimiento de la información personal, pero la plantilla del proyecto incluye la
función de consentimiento en caso de que sea necesaria para cumplir con el Reglamento general de
protección de datos (RGPD ) de la Unión Europea.

En la siguiente imagen se muestra la aplicación tras haber dado su consentimiento al seguimiento:


Examen de los archivo del proyecto
He aquí un resumen de las principales carpetas y archivos del proyecto con los que va a trabajar en los próximos
tutoriales.
Carpeta Pages
Contiene Razor Pages y los archivos auxiliares. Cada página de Razor se compone de un par de archivos:
Archivo .cshtml que contiene el marcado HTML con código C# que usa la sintaxis Razor.
Archivo . cshtml.cs que contiene C# código que controla los eventos de página.
Los archivos auxiliares tienen nombres que comienzan con un carácter de subrayado. Por ejemplo, el archivo
_Layout.cshtml configura los elementos de la interfaz de usuario comunes a todas las páginas. Este archivo
configura el menú de navegación de la parte superior de la página y el aviso de copyright de la parte inferior de la
página. Para más información, consulte Diseño en ASP.NET Core.
Carpeta wwwroot
Contiene los archivos estáticos, como los archivos HTML, los archivos de JavaScript y los archivos CSS. Para más
información, consulte Archivos estáticos en ASP.NET Core.
appSettings.json
Contiene los datos de configuración, como las cadenas de conexión. Para más información, consulte Configuración
en ASP.NET Core.
Program.cs
Contiene el punto de entrada del programa. Para más información, consulte Host genérico de .NET.
Startup.cs
Contiene código que configura el comportamiento de la aplicación, como, por ejemplo, si se requiere
consentimiento para las cookies. Para más información, consulte Inicio de la aplicación en ASP.NET Core.

Recursos adicionales
Versión en YouTube de este tutorial

Pasos siguientes
En este tutorial ha:
Creado una aplicación web de Razor Pages.
Ejecutado la aplicación.
Examinado los archivo del proyecto.
Pase al siguiente tutorial de la serie:

A GREGA R UN
M ODELO
Agregar un modelo a una aplicación de páginas de
Razor en ASP.NET Core
10/05/2019 • 20 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En esta sección, se agregan 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.
Las clases de modelo 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.
Vea o descargue un ejemplo.

Agregar un modelo de datos


Visual Studio
Visual Studio Code
Visual Studio para Mac
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
Película.
Agregue las propiedades siguientes a la clase Movie :

using System;
using System.ComponentModel.DataAnnotations;

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

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

la clase Movie contiene:


La base de datos requiere el campo ID para la clave principal.
[DataType(DataType.Date)] : El atributo DataType especifica el tipo de datos (Date). Con este atributo:
El usuario no tiene que especificar información horaria en el campo de fecha.
Solo se muestra la fecha, no información horaria.
Los elementos DataAnnotations se tratan en un tutorial posterior.
Compile el proyecto para comprobar que no haya errores de compilación.

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
Cree una carpeta Pages/Movies:
Haga clic con el botón derecho en la carpeta Páginas > Agregar > Nueva carpeta.
Asigne a la carpeta el nombre Movies.
Haga clic con el botón derecho en la carpeta Pages/Movies > Agregar > Nuevo elemento con scaffolding.

En el cuadro de diálogo Agregar scaffold, seleccione Páginas de Razor 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.
Seleccione Agregar.

El archivo appsettings.json se actualiza con la cadena de conexión que se usa para conectarse a una base de datos
local.
El proceso de scaffolding crea y actualiza los archivos siguientes:
Archivos creados
Pages/Movies: Create, Delete, Details, Edit e Index.
Data/RazorPagesMovieContext.cs
Archivo actualizado
Startup.cs
Los archivos creados y actualizados se explican en la sección siguiente.
Migración inicial
Visual Studio
Visual Studio Code
Visual Studio para Mac
En esta sección, la Consola del administrador de paquetes (PMC ) se utiliza 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

Los comandos anteriores generan la advertencia siguiente: "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 'HasColumnType()'."
("No se ha especificado ningún tipo en la columna decimal 'Price' en el tipo de entidad 'Movie'. Esto hará que los
valores se trunquen inadvertidamente si no caben según la precisión y escala predeterminados. Especifique
expresamente el tipo de columna de SQL Server que tenga cabida para todos los valores usando
'HasColumnType()'.")
Puede omitir dicha advertencia, ya que se corregirá en un tutorial posterior.
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 RazorPagesMovieContext.cs. El argumento
InitialCreate se usa para asignar nombre a las migraciones. Se puede usar cualquier nombre, pero, por
convención, se selecciona uno que describa la migración.
El comando ef database update ejecuta el método Up en el archivo Migrations/<time-stamp>_InitialCreate.cs. El
método Up crea la base de datos.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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:

// 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_2);

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

El elemento RazorPagesMovieContext coordina la funcionalidad de EF Core (creación, lectura, actualización,


eliminación, etc.) para el modelo Movie . El contexto de datos ( RazorPagesMovieContext ) se deriva de
Microsoft.EntityFrameworkCore.DbContext. En el contexto de datos se especifica qué entidades se incluyen en el
modelo de datos.

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


}
}

El código anterior 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.
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. Se puede usar cualquier nombre, pero, por convención, se
utiliza uno que describa la migración. Para obtener más información, vea Tutorial: Uso de la característica de
migraciones: ASP.NET MVC con EF Core.
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.
Prueba de la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).

Si se produce un error:

SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.

Quiere decir que falta el paso de migraciones.


Pruebe el vínculo Crear.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . La aplicación debe globalizarse 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. Para obtener instrucciones sobre la
globalización, consulte esta cuestión en GitHub.

Pruebe los vínculos Editar, Detalles y Eliminar.


En el tutorial siguiente se explican los archivos creados mediante scaffolding.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : S IG U IE N T E : R A Z O R P A G E S C O N
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
10/05/2019 • 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 de creación, eliminación, detalles y edición


Examine el modelo de página Pages/Movies/Index.cshtml.cs:

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.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 RazorPagesMovieContext 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";
}

<h1>Index</h1>

<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.DisplayFor de la página.
Página de diseño
Seleccione los vínculos de menú (RazorPagesMovie [Película de Razor Pages], Home [Inicio] y Privacy
[Privacidad]). Cada página muestra el mismo diseño de menú. El diseño de menú se implementa en el archivo
Pages/Shared/_Layout.cshtml. Abra el archivo Pages/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 vistas específicas de página que cree, encapsuladas en la página de diseño. Por ejemplo, si
selecciona el vínculo Privacy (Privacidad), la vista Pages/Privacy.cshtml se representará dentro del método
RenderBody .

Propiedades ViewData y Layout


Tenga en cuenta el siguiente código del archivo Pages/Movies/Index.cshtml:

@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 _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 que no aparece en el archivo de diseño. A
diferencia de los comentarios HTML ( <!-- --> ), los comentarios de Razor no se envían al cliente.
Actualizar el diseño
Cambie el elemento <title> del archivo Pages/Shared/_Layout.cshtml para mostrar Movie en lugar de
RazorPagesMovie.

<!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/Shared/_Layout.cshtml.

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

Reemplace el elemento anterior por el marcado siguiente.

<a class="navbar-brand" asp-page="/Movies/Index">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 . El valor de atributo asp-area está vacío, por lo que no se usa el área del vínculo. Consulte Áreas
para obtener más información.
Guarde los cambios y pruebe la aplicación haciendo clic en el vínculo RpMovie. Si tiene cualquier problema,
consulte el archivo _Layout.cshtml en GitHub.
Pruebe los otros vínculos (Inicio, RpMovie, 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.

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.
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;
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, hablaremos 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";
}

<h1>Create</h1>

<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-primary" />
</div>
</form>
</div>
</div>

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

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

Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio muestra la etiqueta <form method="post"> con una fuente negrita diferenciada que se aplica a los
asistentes de etiquetas:
El elemento <form method="post"> es un asistente de etiquetas de formulario. El asistente 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>

Los asistentes 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.
El asistente 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 .
El asistente 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.

Recursos adicionales
Versión en YouTube de este tutorial
A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E : B A S E D E
M ODELO D A TOS
Trabajar con una base de datos y ASP.NET Core
10/05/2019 • 14 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


Vea o descargue el código de ejemplo (cómo descargarlo).
El objeto RazorPagesMovieContext 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 de Startup.cs:
Visual Studio
Visual Studio Code
Visual Studio para Mac

// 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_2);

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-
1234;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Cuando la aplicación se implementa en un servidor de prueba o producción, se puede utilizar una variable de
entorno para establecer la cadena de conexión en un servidor de base de datos real. Para más información, vea
Configuración.
Visual Studio
Visual Studio Code
Visual Studio para Mac

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

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:
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models con el código 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 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.

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
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : R A Z O R P A G E S C O N S IG U IE N T E : A C T U A L IZ A C IÓ N D E
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
10/05/2019 • 9 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas con scaffolding pinta bien, pero la presentación no es ideal. FechaDeLanzamiento debe
ser Fecha de lanzamiento (tres 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;
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; }
}
}

La anotación de datos [Column(TypeName = "decimal(18, 2)")] permite que Entity Framework Core asigne
correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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, el ?id=1 de https://localhost:5001/Movies/Details?id=1 ).
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?}"

Para probar el comportamiento de @page "{id:int?}" :


Establezca la directiva de página de Pages/Movies/Details.cshtml en @page "{id:int?}" .
Establezca un punto de interrupción en public async Task<IActionResult> OnGetAsync(int? id) (en
Pages/Movies/Details.cshtml.cs).
Navegue a https://localhost:5001/Movies/Details/ .

Con la directiva @page "{id:int}" , el punto de interrupción nunca se alcanza. El motor de enrutamiento devuelve
HTTP 404. Con @page "{id:int?}" , el método OnGetAsync devuelve NotFound ( HTTP 404 ).

Aunque no se recomienda, puede escribir el método OnGetAsync (en Pages/Movies/Delete.cshtml.cs) como:

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


{
if (id == null)
{
Movie = await _context.Movie.FirstOrDefaultAsync();
}
else
{
Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
}

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

Pruebe el código anterior:


Seleccione un vínculo Eliminar.
Quite el identificador de la dirección URL. Por ejemplo, cambie https://localhost:5001/Movies/Delete/8 a
https://localhost:5001/Movies/Delete .
Ejecute paso a paso el código del depurador.
Revisión del control de excepciones de simultaneidad
Revise el método OnPostAsync en el archivo Pages/Movies/Edit.cshtml.cs:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

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

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

return RedirectToPage("./Index");
}

private bool MovieExists(int id)


{
return _context.Movie.Any(e => e.ID == id);
}

El código anterior detecta las excepciones de simultaneidad cuando el cliente uno elimina la película y el otro cliente
publica cambios en ella.
Para probar el bloque catch :
Establecer un punto de interrupción en catch (DbUpdateConcurrencyException)
Seleccione Editar para una película y realice cambios, pero no seleccione Guardar.
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.
Es posible que el código de producción quiera detectar conflictos de simultaneidad. 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 RazorPagesMovieContext _context;

public EditModel(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 muestra 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.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : T R A B A J O C O N U N A B A S E D E S IG U IE N T E : A D IC IÓ N D E
D A TOS BÚSQUEDA
Agregar búsqueda a páginas de Razor de ASP.NET
Core
10/05/2019 • 8 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En las secciones siguientes, se ha agregado la función de buscar películas por género o nombre.
Agregue las siguientes propiedades resaltadas a Pages/Movies/Index.cshtml.cs:

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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


[BindProperty(SupportsGet = true)]
public string SearchString { get; set; }
// Requires using Microsoft.AspNetCore.Mvc.Rendering;
public SelectList Genres { get; set; }
[BindProperty(SupportsGet = true)]
public string MovieGenre { get; set; }

SearchString : contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda. El elemento
SearchString está decorado con el atributo [BindProperty] . [BindProperty] enlaza los valores del formulario y
las cadenas de consulta con el mismo nombre que la propiedad. (SupportsGet = true) se necesita para el enlace
de las solicitudes GET.
Genres : contiene la lista de géneros. Genres permite al usuario seleccionar un género de la lista. SelectList
requiere using Microsoft.AspNetCore.Mvc.Rendering; .
MovieGenre : contiene el género concreto que selecciona el usuario (por ejemplo, "Western").
Genres y MovieGenre se utilizan posteriormente en este tutorial.

WARNING
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 el enlace de GET , 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)]

Actualice el método OnGetAsync de la página de índice con el código siguiente:


public async Task OnGetAsync()
{
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:

// using System.Linq;
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 la propiedad SearchString no es NULL ni está vacía, 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, https://localhost:5001/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, https://localhost:5001/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.
El entorno de ejecución de ASP.NET Core usa el enlace de modelos para establecer el valor de la propiedad
SearchString de la cadena de consulta ( ?searchString=Ghost ) o de los datos de ruta (
https://localhost:5001/Movies/Ghost ). El enlace de modelos no hace distinción entre mayúsculas y minúsculas.

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

<h1>Index</h1>

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

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

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

La etiqueta HTML <form> usa los siguientes Asistentes de etiquetas:


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 a través de la cadena de consulta.
Asistente de etiquetas de entrada
Guarde los cambios y pruebe el filtro.
Búsqueda por género
Actualice el método OnGetAsync con el código siguiente:

public async Task OnGetAsync()


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

Agregar búsqueda por género a la página de Razor


Actualice Index.cshtml como se indica a continuación:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h1>Index</h1>

<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" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>

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

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

Recursos adicionales
Versión en YouTube de este tutorial

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 : Adición de un nuevo campo


P Á G IN A S
Agregar un campo nuevo a una página de Razor en
ASP.NET Core
10/05/2019 • 10 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En esta sección, Migraciones de Entity Framework Code First se utiliza para:
Agregar un campo nuevo al modelo.
Migrar el cambio de esquema del campo nuevo 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; }

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


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

Compile la aplicación.
Edite Pages/Movies/Index.cshtml y agregue un campo Rating :

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h1>Index</h1>

<p>
<a asp-page="Create">Create New</a>
<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" asp-for="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>

Actualice las páginas siguientes:


Agregue el campo Rating a las páginas Delete y Details.
Actualice Create.cshtml con un campo Rating .
Agregue el campo Rating a la página de edición.

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

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

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

Vea el archivo completado SeedData.cs.


Compile la solución.
Visual Studio
Visual Studio Code/Visual Studio para Mac
Agregar una migración para el campo de clasificació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.
El comando Update-Database le indica al marco que aplique los cambios de esquema a la base de datos.
Si elimina todos los registros de la base de datos, el inicializador inicializará la base de datos e incluirá 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).
Otra opción es eliminar la base de datos y usar las migraciones para volver a crear la base de datos. Para eliminar la
base de datos de 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 se ha propagado, establezca un punto de interrupción en el método SeedData.Initialize .

Recursos adicionales
Versión en YouTube de este tutorial

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
BÚSQUEDA V A L ID A C IÓ N
Agregar la validación a una página de Razor de
ASP.NET Core
19/05/2019 • 14 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.
Hacer 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.

Add validation rules to the movie model


Open the Movie.cs file. The DataAnnotations namespace provides a set of built-in validation attributes that are
applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType that
help with formatting and don't provide any validation.
Update the Movie class to take advantage of the built-in Required , StringLength , RegularExpression , and Range
validation attributes.
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; }
}

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing prevents
a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for a
"Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures
that you can't forget to validate something and inadvertently let bad data into the database.
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 decimales en campos decimales. 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 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. Puede hacerlo con las herramientas para desarrolladores del
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)]


[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
Para obtener información sobre la implementación en Azure, consulte Tutorial: Compilación de una aplicación
ASP.NET en Azure con SQL Database. Estas instrucciones son para una aplicación ASP.NET, no para una
aplicación ASP.NET Core, pero los pasos son los mismos.
Gracias por seguir esta introducción a las páginas de Razor. Introducción a MVC con Razor Pages y EF Core es un
excelente artículo de seguimiento de este tutorial.

Recursos adicionales
Asistentes de etiquetas en formularios de ASP.NET Core
Globalización y localización en ASP.NET Core
Asistentes de etiquetas en ASP.NET Core
Crear asistentes de etiquetas en ASP.NET Core
Versión en YouTube de este tutorial

A N T E R IO R : Adición de un nuevo campo


Creación de una aplicación web con MVC de
ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Si todavía no tiene
experiencia en el desarrollo web de ASP.NET Core, considere la versión de Razor Pages de este tutorial, que
proporciona un punto de partida más sencillo.
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
04/07/2019 • 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. Si todavía no tiene
experiencia en el desarrollo web de ASP.NET Core, considere la versión de Razor Pages de este tutorial, que
proporciona un punto de partida más sencillo.
En este tutorial se enseñan los conceptos básicos de la compilación de una aplicación web ASP.NET Core MVC.
La aplicación administra una base de datos de títulos de películas. Aprenderá a:
Crear una aplicación web.
Agregar un modelo y aplicarle scaffolding.
Trabajar con una base de datos.
Agregar búsqueda y validación.
Al final, tendrá una aplicación que le permitirá administrar y mostrar datos de películas.
Vea o descargue el código de ejemplo (cómo descargarlo).

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.

Creación de una aplicación web


Visual Studio
Visual Studio Code
Visual Studio para Mac
En Visual Studio, seleccione Crear un proyecto.
Seleccione Aplicación web de ASP.NET Core y, luego, Siguiente.
Asigne el nombre MvcMovie al proyecto y seleccione Crear. Es importante que el proyecto se llame
MvcMovie para que, al copiar el código, coincida con el espacio de nombres.

Seleccione Aplicación web (Modelo-Vista-Controlador) y, luego, Crear.


Visual Studio ha usado la 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.
Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl-F5 para ejecutar la aplicación en modo de no depuración.
Visual Studio muestra el cuadro de diálogo siguiente:

Haga clic en Sí si confía en el certificado SSL de IIS Express.


Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener más información, vea Confiar en el certificado de desarrollo de ASP.NET Core HTTPS .
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.
El inicio de la aplicación con Ctrl+F5 (modo de no depuración) permite realizar 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 seleccionando el botón IIS Express.


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
Visual Studio Code
Visual Studio para Mac

Ayuda de Visual Studio


Información sobre cómo depurar código de C# con Visual Studio
Introducción al IDE de Visual Studio
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
18/06/2019 • 12 minutes to read • Edit Online

Por Rick Anderson


El patrón de arquitectura de Modelo-Vista-Controlador (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 (IU ) de la aplicación. Por lo
general, esta interfaz de usuario muestra los datos del modelo.
Controladores: 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,
https://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). https://localhost:1234/Movies/Edit/5 es una
solicitud para editar la película con ID=5 mediante el controlador de películas. Los datos de ruta se explican
más adelante en el 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.

Incorporación de un controlador
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el Explorador de soluciones, haga clic con el botón derecho en Controladores > Agregar >
Controlador .
En el cuadro de diálogo Agregar Scaffold, seleccione MVC Controller - Empty (Controlador MVC: en
blanco)

En el cuadro de diálogo Add Empty MVC Controller (Agregar controlador MVC en blanco), escriba
HelloWorldController y seleccione AGREGAR.
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
https://localhost:5001/HelloWorld . Combina el protocolo usado HTTPS , la ubicación de red del servidor web
(incluido el puerto TCP ) localhost:5001 y el URI de destino HelloWorld .
El primer comentario indica que se trata de un método HTTP GET que se invoca anexando /HelloWorld/ a la
dirección URL base. El segundo comentario especifica un método HTTP GET que se invoca anexando
/HelloWorld/Welcome/ a la dirección URL. Más adelante en el tutorial se usa el motor de scaffolding para generar
métodos HTTP POST que actualizan los datos.
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 de enrutamiento se establece en el método Configure del archivo Startup.cs.

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

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


predeterminada se usan 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. Los datos de ruta se
explican más adelante en el tutorial.
Vaya a https://localhost:xxxx/HelloWorld/Welcome. El método Welcome se ejecuta y devuelve la cadena
This is the Welcome action method... . 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 (en concreto
JavaScript).
Usa cadenas interpoladas en $"Hello {name}, NumTimes is: {numTimes}" .

Ejecute la aplicación y navegue a:


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

(Reemplace xxxx con el número de puerto). Puede probar distintos valores 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 direcciones 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 siguiente dirección URL: https://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 elemento ? 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
03/07/2019 • 16 minutes to read • Edit Online

Por Rick Anderson


En esta sección, se modificará la clase HelloWorldController para usar los archivos de vista de 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 con 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 llama al método View del controlador. Este usa una plantilla de vista para generar una respuesta
HTML. Los métodos de controlador (también conocidos como métodos de acción), como el método Index
anterior, suelen devolver un valor IActionResult o una clase derivada de ActionResult, en lugar de un tipo como una
cadena string .

Agregar una vista


Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
Seleccione Vista de Razor.
Conserve el valor del cuadro Nombre, Index.cshtml.
Seleccione 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 https://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. Dado que no se especificó un nombre de archivo de plantilla de vista, MVC
usa el archivo de vista predeterminado. Este archivo tiene el mismo nombre que el método ( Index ), por lo que se
usa en /Views/HelloWorld/Index.cshtml. 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.
Cambio de vistas y páginas de diseño
Seleccione los vínculos de menú (MvcMovie [Película de MVC ], Home [Inicio] y Privacy [Privacidad]). 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 Privacy (Privacidad), la vista Views/Home/Privacy.cshtml se representa dentro del método
RenderBody .

Cambio de los vínculos del título, el pie de página y el menú en el


archivo de diseño
En los elementos de título y pie de página, cambie MvcMovie por Movie App .
Cambie el delimitador
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a> por
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a> .

En el marcado siguiente se muestran los cambios:

<!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 include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/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"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow
mb-3">
<div class="container">
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-
collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2019 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

@RenderSection("Scripts", required: false)


</body>
</html>

En el marcado anterior, se omitió el atributo del asistente de etiquetas delimitadoras asp-area porque esta
aplicación no utiliza Áreas.
Nota: El controlador Movies no se ha implementado. En este momento, el vínculo Movie App no es funcional.
Guarde los cambios y seleccione el vínculo Privacy (Privacidad). Observe cómo el título de la pestaña del
explorador muestra ahora Privacy Policy - Movie Ap (Directiva de privacidad - Aplicación de película) en lugar
de Privacy Policy - Mvc Movie (Directiva de privacidad - Aplicación de MVC ):
Pulse el vínculo Home (Inicio) y observe que el texto del título y el delimitador también muestran Movie App
(Aplicación de película). 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. Se 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 y el elemento <h2> del archivo de vista Views/HelloWorld/Index.cshtml:

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

<h2>My Movie List</h2>

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

El título y el elemento <h2> son algo diferentes para que pueda ver qué parte del código cambia la presentación.
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 https://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.
En HelloWorldController.cs, 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 utilizarse cualquier tipo; 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:


https://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, se usó 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 . Consulte When
to use ViewBag, ViewData, or TempData (Cuándo usar ViewBag, ViewData o TempData) para más información.
En el tutorial siguiente crearemos 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
17/06/2019 • 23 minutes to read • Edit Online

Por Rick Anderson y Tom Dykstra


En esta sección, agregará las 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.
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 del modelo y EF Core crea la base de datos. Existe un enfoque
alternativo que no trataremos 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


Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Models > Agregar > Clase. Asigne a la clase el nombre Película.
Agregue las propiedades siguientes a la clase Movie :

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }

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

la clase Movie contiene:


El campo Id , que requiere la base de datos para la clave principal.
[DataType(DataType.Date)] : el atributo DataType especifica el tipo de datos ( Date ). Con este atributo:
El usuario no tiene que especificar información horaria en el campo de fecha.
Solo se muestra la fecha, no información horaria.
Los elementos DataAnnotations se tratan en un tutorial posterior.

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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, seleccione 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.
Seleccione 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.
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 usar para ello 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.

Migración inicial
En esta sección, se completan las tareas siguientes:
Agregar una migración inicial.
Actualizar la base de datos con la migración inicial.
Visual Studio
Visual Studio Code/Visual Studio para Mac
1. En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del
Administrador de paquetes (PMC ).
2. En PCM, escriba los siguientes comandos:

Add-Migration Initial
Update-Database

El comando Add-Migration genera el código para crear el esquema de base de datos inicial.
El esquema de la base de datos se basa en el modelo especificado en la clase MvcMovieContext . El argumento
Initial es el nombre de la migración. Se puede usar cualquier nombre, pero, por convención, se utiliza uno
que describa la migración. Para obtener más información, vea Tutorial: Uso de la característica de
migraciones: ASP.NET MVC con EF Core.
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.

Examinar el contexto registrado con la inserción de dependencias


ASP.NET Core integra la inserción de dependencias (DI). 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.
Visual Studio
Visual Studio Code/Visual Studio para Mac
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.
Consulte el siguiente método Startup.ConfigureServices . 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_2);

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

El elemento MvcMovieContext coordina la funcionalidad de EF Core (creación, lectura, actualización, eliminación,


etc.) para el modelo Movie . El contexto de datos ( MvcMovieContext ) se deriva de
Microsoft.EntityFrameworkCore.DbContext. En el contexto de datos se especifica qué entidades se incluyen en el
modelo de datos:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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; }


}
}

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.
Prueba de la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).

Si se produce una excepción de base de datos similar a la siguiente:

SqlException: Cannot open database "MvcMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.

Quiere decir que falta el paso de migraciones.


Pruebe el vínculo Crear. Escriba y envíe los datos.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . La aplicación debe globalizarse 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. Para obtener instrucciones sobre la
globalización, consulte esta cuestión en GitHub.

Pruebe los vínculos Editar, Detalles y Eliminar.


Examine la clase Startup :

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

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

En el código resaltado anterior se muestra cómo se agrega el contexto de base de datos de películas al contenedor
de inserción de dependencias:
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);
}

El parámetro id suele pasarse como datos de ruta. Por ejemplo, https://localhost:5001/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:
https://localhost:5001/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);

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

<h1>Details</h1>

<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Genre)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class="col-sm-10">
@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. Al crear el controlador de película, se 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";
}

<h1>Index</h1>

<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 implica 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
Trabajo con SQL en ASP.NET Core
10/05/2019 • 8 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:
Visual Studio
Visual Studio Code/Visual Studio para Mac

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

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.
Visual Studio
Visual Studio Code/Visual Studio para Mac

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

Prueba de la aplicación
Visual Studio
Visual Studio Code/Visual Studio para Mac
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
10/05/2019 • 16 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas tiene buena pinta, pero la presentación no es la ideal. Por ejemplo,
FechaDeLanzamiento debería escribirse en tres 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; }
}
}

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 https://localhost:5001/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 obtener
más información, consulte 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, consulte 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 FindAsync 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);
}

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

<h1>Edit</h1>

<h4>Movie</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="Id" />
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

<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 publique en el servidor, la validación del lado cliente comprueba las reglas 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
10/05/2019 • 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?}");
});

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 el asistente 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 el asistente 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, se
especifica que la solicitud sea HTTP GET :
@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)

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">

Adición de 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 { get; set; }
public SelectList Genres { get; set; }
public string MovieGenre { get; set; }
public string SearchString { 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 permite al usuario seleccionar un género de la lista.
MovieGenre , que contiene el género seleccionado.
SearchString , que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

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

// GET: Movies
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


{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
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).
Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice


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

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

<h1>Index</h1>

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

En el código anterior, 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. 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 buscando por género, por título de la película y por 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
18/06/2019 • 9 minutes to read • Edit Online

Por Rick Anderson


En esta sección, Migraciones de Entity Framework Code First se utiliza para:
Agregar un campo nuevo al modelo.
Migrar el nuevo campo a la base de datos.
Al usar Code First de EF para crear una base de datos automáticamente, Code First hace lo siguiente:
Agrega una tabla a la base de datos para realizar un seguimiento del esquema de la base de datos.
Comprueba que la base de datos está sincronizada con las clases del modelo desde las 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


Agregue una Rating propiedad a Models/Movie.cs:

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


Dado que ha agregado un nuevo campo a la clase Movie , debe actualizar la lista de enlaces permitidos 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")]

Actualice 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 :
<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>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |

Actualice /Views/Movies/Create.cshtml con un campo Rating .


Visual Studio/Visual Studio para Mac
Visual Studio Code
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.
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
},

La aplicación no funciona hasta que la base de datos se actualiza para incluir el nuevo campo. Si se ejecuta ahora,
se produce la siguiente SqlException :
SqlException: Invalid column name 'Rating'.

Este error se produce porque la clase del modelo Movie actualizada es diferente a la 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. 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 realiza 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. Se trata de un buen enfoque para el desarrollo inicial y cuando se
usa SQLite.
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.
En este tutorial se usa Migraciones de Code First.
Visual Studio
Visual Studio Code/Visual Studio para Mac
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 se eliminan todos los registros de la base de datos, el método de inicialización inicializa la base de datos e incluye
el campo Rating .
Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . 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
17/06/2019 • 17 minutes to read • Edit Online

Por Rick Anderson


En esta sección:
Se agrega lógica de validación al modelo Movie .
Asegúrese de que las reglas de validación se aplican cada vez 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.

Add validation rules to the movie model


Open the Movie.cs file. The DataAnnotations namespace provides a set of built-in validation attributes that are
applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType that
help with formatting and don't provide any validation.
Update the Movie class to take advantage of the built-in Required , StringLength , RegularExpression , and Range
validation attributes.
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; }
}

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing prevents
a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for a
"Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures
that you can't forget to validate something and inadvertently let bad data into the database.

UI de error de validación
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 campos decimales. 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.

La parte de la plantilla de visualización Create.cshtml se muestra en el marcado siguiente:


<h4>Movie</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="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>

@*Markup removed for brevity.*@

Los métodos de acción utilizan el marcado anterior para mostrar el formulario inicial y para volver a mostrarlo en
caso de error.
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; }
}

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
10/05/2019 • 5 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);
}

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 FirstOrDefaultAsync . 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));
}

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 los segmentos de una dirección URL a los métodos de acción por nombre, de modo que si cambia el
nombre de un método, el enrutamiento seguramente no podrá encontrar dicho 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
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

Publicar en Azure
Para obtener información sobre la implementación en Azure, consulte Tutorial: Creación de una aplicación .NET
Core y SQL Database en Azure App Service.

A N T E R IO R
Creación de la primera aplicación Blazor
17/06/2019 • 14 minutes to read • Edit Online

Por Daniel Roth y Luke Latham


En este tutorial se muestra cómo crear y modificar una aplicación de Blazor.
Siga las instrucciones del artículo Get started with ASP.NET Core Blazor para crear un proyecto de Blazor en este
tutorial.

Creación de componentes
1. Vaya a cada una de las tres páginas de la aplicación en la carpeta Pages: Home (Inicio), Counter (Contador) y
Fetch data (Recuperar datos). Estas páginas se implementan mediante los archivos de componente de Razor
Index.razor, Counter.razor y FetchData.razor.
2. En la página Contador, seleccione el botón Click me para aumentar el contador sin una actualización de
página. Aumentar un contador en una página web suele requerir la escritura de JavaScript, pero Blazor
proporciona una mejor manera de usar C#.
3. Examine la implementación del componente Counter en el archivo Counter.razor.
Pages/Counter.razor:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

la interfaz de usuario del componente Counter se define mediante HTML. La lógica de la representación
dinámica (por ejemplo, bucles, instrucciones condicionales, expresiones) se agrega mediante una sintaxis de
C# insertada denominada Razor. El marcado HTML y la lógica de representación de C# se convierten en
una clase de componente en tiempo de compilación. El nombre de la clase de .NET generada coincide con el
nombre del archivo.
Los miembros de la clase de componente se definen en un bloque @code . En el bloque @code , se especifica
el estado del componente (propiedades, campos) y los métodos para el tratamiento de eventos o para definir
otra lógica del componente. Estos miembros se utilizan como parte de la lógica de representación del
componente y para el tratamiento de eventos.
Al seleccionarse el botón Click me:
Se llama al controlador onclick registrado del componente Counter (el método IncrementCount ).
El componente Counter regenera su árbol de representación.
El nuevo árbol de representación se compara con el anterior.
Únicamente se aplican modificaciones en Document Object Model (DOM ). Se actualiza el recuento
mostrado.
4. Modifique la lógica de C# del componente Counter para hacer que el recuento se incremente en dos en
lugar de uno.

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount += 2;
}
}

5. Recompile y ejecute la aplicación para ver los cambios. Seleccione el botón Hacer clic aquí. El contador se
incrementa en dos.

Uso de componentes
Incluya un componente en otro componente mediante una sintaxis HTML.
1. Agregue el componente Counter al componente Index de la aplicación; para ello, agregue un elemento
<Counter /> al componente Index ( Index.razor).

Si usa Blazor para esta experiencia, hay un componente Survey Prompt (elemento <SurveyPrompt> ) en el
componente Index. Reemplace el elemento <SurveyPrompt> por el elemento <Counter> . Si usa una
aplicación de servidor de Blazor para esta experiencia, agregue el elemento <Counter> al componente Index:
Pages/Index.razor:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<Counter />

2. Recompile y ejecute la aplicación. El componente Index tiene su propio contador.

Parámetros del componente


Los componentes también pueden tener parámetros. Los parámetros del componente se definen mediante
propiedades privadas en la clase de componentes decorada con [Parameter] . Use atributos para especificar
argumentos para un componente en el marcado.
1. Actualice el código de C# @code del componente:
Agregue una propiedad IncrementAmount decorada con el atributo [Parameter] .
Cambie el método IncrementCount para usar IncrementAmount al aumentar el valor de currentCount .
Pages/Counter.razor:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

[Parameter]
private int IncrementAmount { get; set; } = 1;

private void IncrementCount()


{
currentCount += IncrementAmount;
}
}

1. Especifique un parámetro IncrementAmount en el elemento <Counter> del componente Index mediante un


atributo. Establezca el valor para incrementar el contador en diez.
Pages/Index.razor:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<Counter IncrementAmount="10" />

2. Vuelva a cargar el componente Index. El contador se incrementa en diez cada vez que se selecciona el botón
Click me. El contador del componente Counter sigue incrementándose en uno.

Enrutamiento a los componentes


La directiva @page en la parte superior del archivo Counter.razor especifica que el componente Counter es un
punto de conexión de enrutamiento. El componente Counter controla las solicitudes enviadas a /counter . Sin la
directiva @page , el componente no controla las solicitudes enrutadas, pero otros componentes aún pueden usar el
componente.

Inserción de dependencias
Los servicios registrados en el contenedor de servicios de la aplicación están disponibles para los componentes
mediante una inserción de dependencia (DI). Inserte servicios en un componente mediante la directiva @inject .
Examine las directivas del componente FetchData.
Si trabaja con la aplicación de servidor de Blazor, el servicio WeatherForecastService se registra como singleton, de
modo que una instancia del servicio está disponible en toda la aplicación. La directiva @inject se usa para insertar
la instancia del servicio WeatherForecastService en el componente.
Pages/FetchData.razor:

@page "/fetchdata"
@using WebApplication1.App.Services
@inject WeatherForecastService ForecastService

El componente FetchData usa el servicio insertado, como ForecastService , para recuperar una matriz de objetos
WeatherForecast :

@code {
private WeatherForecast[] forecasts;

protected override async Task OnInitAsync()


{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}

Si trabaja con la aplicación cliente de Blazor, se inserta HttpClient para obtener datos de previsión del tiempo del
archivo weather.json de la carpeta wwwroot/sample-data:
Pages/FetchData.razor:

@inject HttpClient Http

...

protected override async Task OnInitAsync()


{
forecasts =
await Http.GetJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}

Se usa un bucle @foreach para representar cada instancia de previsión como una fila de la tabla de datos
meteorológicos:

<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>

Creación de una lista de tareas pendientes


Agregue un nuevo componente a la aplicación que implemente una simple lista de tareas pendientes.
1. Agregue un archivo vacío denominado Todo.razor a la aplicación en la carpeta Pages:
2. Proporcione el marcado inicial para el componente:

@page "/todo"

<h1>Todo</h1>

3. Agregue el componente Todo a la barra de navegación.


El componente NavMenu (Shared/NavMenu.razor) se usa en el diseño de la aplicación. Los diseños son
componentes que le permiten impedir la duplicación de contenido en la aplicación. Para obtener más
información, vea ASP.NET Core Blazor layouts.
Agregue un elemento <NavLink> a la página Todo mediante la adición del siguiente marcado de elementos
de lista debajo de los elementos de lista existentes en el archivo Shared/NavMenu.razor:

<li class="nav-item px-3">


<NavLink class="nav-link" href="todo">
<span class="oi oi-list-rich" aria-hidden="true"></span> Todo
</NavLink>
</li>

4. Recompile y ejecute la aplicación. Visite la nueva página Todo para confirmar que el vínculo al componente
Todo funcione.
5. Agregue un archivo TodoItem.cs a la raíz del proyecto para contener una clase que represente un elemento
de la lista de tareas. Use el siguiente código de C# para la clase TodoItem :

public class TodoItem


{
public string Title { get; set; }
public bool IsDone { get; set; }
}

6. Vuelva al componente Todo (Pages/Todo.razor):


Agregue un campo a los elementos de tareas pendientes en un bloque @code . El componente Todo
utiliza este campo para mantener el estado de la lista de tareas pendientes.
Agregue el marcado de la lista no ordenada y un bucle foreach para que cada elemento de la lista se
represente en un elemento de la lista de tareas pendientes.

@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
}
7. Para agregar elementos de tareas pendientes a la lista, la aplicación requiere elementos de la interfaz de
usuario. Agregue una entrada de texto y un botón debajo de la lista:

@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" />


<button>Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
}

8. Recompile y ejecute la aplicación. Cuando se selecciona el botón Add todo (Agregar tarea pendiente), no
ocurre nada porque no hay ningún controlador de eventos conectado al botón.
9. Agregue un método AddTodo al componente Todo y regístrelo para hacer clic en los botones mediante el
atributo @onclick :

<input placeholder="Something todo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();

private void AddTodo()


{
// Todo: Add the todo
}
}

El método AddTodo de C# se llama cuando se selecciona el botón.


10. Para obtener el título del nuevo elemento de tarea pendiente, agregue un campo de cadena newTodo y
enlácelo al valor de la entrada de texto mediante el atributo bind :

private IList<TodoItem> todos = new List<TodoItem>();


private string newTodo;

<input placeholder="Something todo" @bind="@newTodo" />

11. Actualice el método AddTodo para agregar el TodoItem con el título especificado a la lista. Borre el valor de
la entrada de texto mediante el establecimiento de newTodo en una cadena vacía:
@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" @bind="@newTodo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

12. Recompile y ejecute la aplicación. Agregue algunos elementos de tareas pendientes a la lista de tareas
pendientes para probar el nuevo código.
13. Se puede hacer que el texto de título de cada elemento de tarea pendiente sea editable y una casilla puede
ayudar al usuario a realizar un seguimiento de los elementos completados. Agregue una entrada de casilla a
cada elemento de tarea pendiente y enlace su valor a la propiedad IsDone . Cambie @todo.Title a un
elemento <input> enlazado a @todo.Title :

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>

14. Para comprobar que estos valores están enlazados, actualice el encabezado <h1> para mostrar un recuento
del número de elementos de la lista de tareas pendientes que no se han completado ( IsDone es false ).

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

15. El componente Todo completado (Pages/Todo.razor):


@page "/todo"

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>

<input placeholder="Something todo" @bind="@newTodo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

16. Recompile y ejecute la aplicación. Agregue elementos de tarea pendiente para probar el nuevo código.

Publicar e implementar la aplicación


Para publicar la aplicación, consulte Hospedaje e implementación de ASP.NET Core Blazor.
Tutorial: Creación de una API web con ASP.NET Core
05/07/2019 • 29 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se enseñan los conceptos básicos de la compilación de una API web con ASP.NET Core.
En este tutorial aprenderá a:
Crear un proyecto de API web.
Agregar una clase de modelo.
Crear el contexto de la base de datos.
Registrar el contexto de la base de datos.
Agregar un controlador.
Agregar métodos CRUD.
Configurar el enrutamiento y las rutas de dirección URL.
Especificar los valores devueltos.
Llamar a la API web con Postman.
Llamar a la API web con jQuery.
Al final, tendrá una API web que pueda administrar las tareas "pendientes" almacenadas en una base de datos
relacional.

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

API DESCRIPCIÓN 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 None Tarea pendiente


identificador

POST /api/todo Incorporación de un nuevo Tarea pendiente Tarea pendiente


elemento

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


existente

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

En el diagrama siguiente, se muestra el diseño de la aplicación.


Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.

Creación de un proyecto web


Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú Archivo, seleccione Nuevo > Proyecto.
Seleccione la plantilla Aplicación web ASP.NET Core y haga clic en Siguiente.
Asigne al proyecto el nombre TodoApi y haga clic en Crear.
En el cuadro de diálogo Crear una aplicación web ASP.NET Core, confirme que las opciones .NET Core y
ASP.NET Core 2.2 estén seleccionadas. Seleccione la plantilla API y haga clic en Crear. No seleccione
Habilitar compatibilidad con Docker.
Prueba de la API
La plantilla del proyecto crea una API values . Llame al método Get desde un explorador para probar la
aplicación.
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl+F5 para ejecutar la aplicación. Visual Studio inicia un explorador y navega hasta
https://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente.

Si aparece un cuadro de diálogo en que se le pregunta si debe confiar en el certificado de IIS Express, seleccione Sí.
En el cuadro de diálogo Advertencia de seguridad que aparece a continuación, seleccione Sí.
Se devuelve el siguiente JSON:

["value1","value2"]

Incorporación de una clase de modelo


Un modelo es un conjunto de clases que representan los datos que la aplicación administra. El modelo para esta
aplicación es una clase TodoItem única.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
Haga clic con el botón derecho en la carpeta Models y seleccione Agregar > Clase. Asigne a la clase el
nombre TodoItem y seleccione Agregar.
Reemplace el código de plantilla por el código siguiente:

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

La propiedad Id funciona como clave única en una base de datos relacional.


Las clases de modelo pueden ir en cualquier lugar del proyecto, pero convencionalmente e usa la carpeta Models.

Incorporación de un 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. Esta clase se crea derivándola de la clase Microsoft.EntityFrameworkCore.DbContext .
Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Models y seleccione Agregar > Clase. Asigne a la clase el nombre
TodoContext y haga clic en Agregar.
Reemplace el código de plantilla por el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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


}
}

Registro del contexto de base de datos


En ASP.NET Core, los servicios (como el contexto de la base de datos) deben registrarse con el contenedor de
inserción de dependencias (DI). El contenedor proporciona el servicio a los controladores.
Actualice Startup.cs con el siguiente código resaltado:
// Unused usings removed
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
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.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

// 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
{
// The default HSTS value is 30 days. You may want to change this for
// production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseMvc();
}
}
}

El código anterior:
Elimina las declaraciones using no utilizadas.
Agrega el contexto de base de datos para el contenedor de DI.
Especifica que el contexto de base de datos usará una base de datos en memoria.

Incorporación de un controlador
Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Controllers.
Seleccione Agregar > Nuevo elemento.
En el cuadro de diálogo Agregar nuevo elemento, seleccione la plantilla Clase de controlador de API.
Asigne a la clase el nombre TodoController y seleccione Agregar.

Reemplace el código de plantilla por el código siguiente:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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.
Representa la clase con el atributo [ApiController]. Este atributo indica que el controlador responde a las
solicitudes de la API web. Para información sobre comportamientos específicos que permite el atributo, consulte
Creación de API web con ASP.NET Core.
Utiliza 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.
Si la base de datos está vacía, le agrega un elemento denominado Item1 . Este código está en el constructor, de
manera que se ejecuta cada vez que hay una nueva solicitud HTTP. Si elimina todos los elementos, el
constructor volverá a crear Item1 la próxima vez que se llame a un método de API. De este modo, es posible
que parezca que la eliminación no ha funcionado, cuando en realidad sí lo ha hecho.

Incorporación de métodos Get


Para proporcionar una API que recupere tareas pendientes, agregue estos métodos a la clase TodoController :

// GET: api/Todo
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
return await _context.TodoItems.ToListAsync();
}

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Estos métodos implementan dos puntos de conexión GET:


GET /api/todo
GET /api/todo/{id}

Llame a los dos puntos de conexión desde un explorador para probar la aplicación. Por ejemplo:
https://localhost:<port>/api/todo
https://localhost:<port>/api/todo/1

La llamada a GetTodoItems genera la siguiente respuesta HTTP:

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

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:
Comience por la cadena de plantilla en el atributo Route del controlador:

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

Reemplace [controller] por el nombre del controlador, que convencionalmente es el nombre de clase de
controlador sin el sufijo "Controller". En este ejemplo, el nombre de clase de controlador es TodoController;
por tanto, el nombre del controlador 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 (por ejemplo, [HttpGet("products")] ), anéxela 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 GetTodoItem , "{id}" es una variable de marcador de posición correspondiente al
identificador único de la tarea pendiente. Al invocar a GetTodoItem , el valor "{id}" de la dirección URL se
proporciona al método en su parámetro id .

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Valores devueltos
El tipo de valor devuelto de los métodos GetTodoItems y GetTodoItem es ActionResult<T > type. ASP.NET Core
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 tipo de valor devuelto es el 200, suponiendo que no haya ninguna excepción no controlada. Las
excepciones no controladas se convierten en errores 5xx.
Los tipos de valores devueltos ActionResult pueden representar una gama amplia de códigos de estado HTTP. Por
ejemplo, GetTodoItem puede devolver dos valores de estado diferentes:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de error
404 NotFound.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.

Prueba del método GetTodoItems


En este tutorial se usa Postman para probar la API web.
Instale Postman.
Inicie la aplicación web.
Inicie Postman.
Deshabilite Comprobación del certificado SSL.
En Archivo > Configuración (pestaña *General), deshabilite Comprobación del certificado SSL.

WARNING
Vuelva a habilitar la comprobación del certificado SSL tras probar el controlador.

Cree una nueva solicitud.


Establezca el método HTTP en GET.
Establezca la dirección URL de la solicitud en https://localhost:<port>/api/todo . Por ejemplo:
https://localhost:5001/api/todo .
Establezca Vista de dos paneles en Postman.
Seleccione Enviar.

Incorporación de un método Create


Agregue el siguiente método PostTodoItem :
// POST: api/Todo
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item);


}

El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El método obtiene el valor de
tareas pendientes del cuerpo de la solicitud HTTP.
El método CreatedAtAction realiza las acciones siguientes:
Devuelve un código de estado HTTP 201 cuando se ha ejecutado correctamente. HTTP 201 es la respuesta
estándar para un método HTTP POST que crea un recurso en el servidor.
Agrega un encabezado Location a la respuesta. El encabezado Location especifica el identificador URI de
la tarea pendiente recién creada. Para obtener más información, consulte 10.2.2 201 creado.
Hace referencia a la acción GetTodoItem para crear el identificador URI del encabezado Location . La
palabra clave nameof de C# se usa para evitar que se codifique de forma rígida el nombre de acción en la
llamada a CreatedAtAction .

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Prueba del método PostTodoItem


Compile el proyecto.
En Postman, establezca el método HTTP en POST .
Seleccione la pestaña Cuerpo.
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json) .
En el cuerpo de la solicitud, introduzca JSON para una tarea pendiente:

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

Seleccione Enviar.
Si recibe un error 405 (Método no permitido), probablemente sea el resultado de no haber compilado el
proyecto después de agregar el método PostTodoItem .
Prueba del URI del encabezado de ubicación
Seleccione la pestaña Encabezados en el panel Respuesta.
Copie el valor de encabezado Ubicación:

Establezca el método en GET.


Pegue el URI (por ejemplo, https://localhost:5001/api/Todo/2 ).
Seleccione Enviar.

Incorporación de un método PutTodoItem


Agregue el siguiente método PutTodoItem :

// PUT: api/Todo/5
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem item)
{
if (id != item.Id)
{
return BadRequest();
}

_context.Entry(item).State = EntityState.Modified;
await _context.SaveChangesAsync();

return NoContent();
}

PutTodoItem es similar a PostTodoItem , 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 cambios. Para admitir actualizaciones parciales, use HTTP PATCH.
Si recibe un error al llamar a PutTodoItem , llame a GET para asegurarse de que hay un elemento en la base de
datos.
Prueba del método PutTodoItem
En este ejemplo se usa una base de datos en memoria que se debe iniciar cada vez que se inicia la aplicación. Debe
haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para asegurarse de que
hay un elemento en la base de datos antes de realizar una llamada PUT.
Actualice la tarea pendiente que tiene el id. = 1 y establezca su nombre en "feed fish":

{
"ID":1,
"name":"feed fish",
"isComplete":true
}

En la imagen siguiente, se muestra la actualización de Postman:


Incorporación de un método DeleteTodoItem
Agregue el siguiente método DeleteTodoItem :

// DELETE: api/Todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();

return NoContent();
}

La respuesta de DeleteTodoItem es 204 (Sin contenido).


Prueba del método DeleteTodoItem
Use Postman para eliminar una tarea pendiente:
Establezca el método en DELETE .
Establezca el URI del objeto que quiera eliminar, por ejemplo, https://localhost:5001/api/todo/1 .
Seleccione Enviar.
La aplicación de ejemplo permite eliminar todos los elementos. Sin embargo, al eliminar el último elemento, se
creará uno nuevo en el constructor de clase de modelo la próxima vez que se llame a la API.

Llamada a la API con jQuery


En esta sección, se agrega una página HTML que usa jQuery para llamar a la API web. jQuery inicia la solicitud y
actualiza la página con los detalles de la respuesta de la API.
Configure la aplicación para atender archivos estáticos y habilitar la asignación de archivos predeterminada
mediante la actualización de Startup.cs con el siguiente código resaltado:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for
// production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

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

Cree una carpeta wwwroot en el directorio del proyecto.


Agregue un archivo HTML denominado index.html al directorio wwwroot. 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">
<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="Save">
<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. 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.text("No " + name);
}
}

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

function getData() {
$.ajax({
type: "GET",
url: uri,
cache: false,
success: function(data) {
const tBody = $("#todos");

$(tBody).empty();

getCount(data.length);
$.each(data, function(key, item) {
const tr = $("<tr></tr>")
.append(
$("<td></td>").append(
$("<input/>", {
type: "checkbox",
disabled: true,
checked: item.isComplete
})
)
)
.append($("<td></td>").text(item.name))
.append(
$("<td></td>").append(
$("<button>Edit</button>").on("click", function() {
editItem(item.id);
})
)
)
.append(
$("<td></td>").append(
$("<button>Delete</button>").on("click", function() {
deleteItem(item.id);
})
)
);

tr.appendTo(tBody);
});

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("Something went wrong!");
},
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
probar la página HTML localmente:
Abra Properties\launchSettings.json.
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.
En este ejemplo se llama a todos los métodos CRUD de la API. A continuación, encontrará algunas explicaciones de
las llamadas a la API.
Obtención de una lista de tareas pendientes
La función de JQuery ajax envía una solicitud GET a la API, que devuelve código JSON que representa una matriz
de tareas pendientes. 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,
cache: false,
success: function(data) {
const tBody = $("#todos");

$(tBody).empty();

getCount(data.length);

$.each(data, function(key, item) {


const tr = $("<tr></tr>")
.append(
$("<td></td>").append(
$("<input/>", {
type: "checkbox",
disabled: true,
checked: item.isComplete
})
)
)
.append($("<td></td>").text(item.name))
.append(
$("<td></td>").append(
$("<button>Edit</button>").on("click", function() {
editItem(item.id);
})
)
)
.append(
$("<td></td>").append(
$("<button>Delete</button>").on("click", function() {
deleteItem(item.id);
})
)
);

tr.appendTo(tBody);
});

todos = data;
}
});
}

Incorporación de una tarea pendiente


La función ajax envía una solicitud POST con la tarea pendiente en su cuerpo. Las opciones accepts y contentType
se establecen en application/json para especificar el tipo de medio que se va a recibir y a enviar. La tarea
pendiente se convierte en JSON mediante 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("Something went wrong!");
},
success: function(result) {
getData();
$("#add-name").val("");
}
});
}

Actualizar una tarea pendiente


El hecho de actualizar una tarea pendiente es similar al de agregar una. El valor url cambia para agregar 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 el valor 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();
}
});

Recursos adicionales
Vea o descargue el código de ejemplo para este tutorial. Vea cómo descargarlo.
Para obtener más información, vea los siguientes recursos:
Creación de API web con ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Páginas de Razor de ASP.NET Core con EF Core: serie de tutoriales
Enrutar a acciones de controlador de ASP.NET Core
Tipos de valor devuelto de acción del controlador de ASP.NET Core Web API
Implementar aplicaciones de ASP.NET Core en Azure App Service
Hospedaje e implementación de ASP.NET Core
Versión en YouTube de este tutorial

Pasos siguientes
En este tutorial ha aprendido a:
Crear un proyecto de API web.
Agregar una clase de modelo.
Crear el contexto de la base de datos.
Registrar el contexto de la base de datos.
Agregar un controlador.
Agregar métodos CRUD.
Configurar el enrutamiento y las rutas de dirección URL.
Especificar los valores devueltos.
Llamar a la API web con Postman.
Llamar a la API web con jQuery.
Pase al siguiente tutorial para obtener información sobre cómo generar páginas de ayuda de API:
Introducción a Swashbuckle y ASP.NET Core
Creación de una API Web con ASP.NET Core y
MongoDB
03/07/2019 • 18 minutes to read • Edit Online

Por Pratik Khandelwal y Scott Addie


En este tutorial se crea una API web que realiza operaciones de creación, lectura, actualización y eliminación
(CRUD ) en una base de datos NoSQL de MongoDB.
En este tutorial aprenderá a:
Configurar MongoDB
Crear una base de datos de MongoDB
Definir un esquema y una colección de MongoDB
Realizar operaciones de CRUD de MongoDB desde una API web
Personalizar la serialización de JSON
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
.NET Core SDK 2.2 o posterior
Visual Studio 2019 con la carga de trabajo ASP.NET y desarrollo web
MongoDB

Configurar MongoDB
Si usa Windows, MongoDB está instalado en C:\Archivos de programa\MongoDB de forma predeterminada.
Agregue C:\Archivos de programa\MongoDB\Server\<número_versión>\bin a la variable de entorno Path . Este
cambio permite el acceso a MongoDB desde cualquier lugar en el equipo de desarrollo.
Use el Shell de mongo en los pasos siguientes para crear una base de datos, hacer colecciones y almacenar
documentos. Para obtener más información sobre los comandos de Shell de mongo, consulte Working with the
mongo Shell (Trabajo con el shell de Mongo).
1. Elija un directorio en el equipo de desarrollo para almacenar los datos. Por ejemplo, C:\BooksData en
Windows. Si no existe el directorio, créelo. El shell de mongo no crea nuevos directorios.
2. Abra un shell de comandos. Ejecute el comando siguiente para conectarse a MongoDB en el puerto
predeterminado 27017. No olvide reemplazar <data_directory_path> por el directorio que eligió en el paso
anterior.

mongod --dbpath <data_directory_path>

3. Abra otra instancia del shell de comandos. Conéctese a la base de datos de prueba de forma predeterminada
ejecutando el comando siguiente:
mongo

4. Ejecute lo siguiente en un shell de comandos:

use BookstoreDb

Si aún no existe, se crea una base de datos denominada BookstoreDb. Si la base de datos existe, su conexión
se abre para las transacciones.
5. Cree una colección Books con el comando siguiente:

db.createCollection('Books')

Se muestra el siguiente resultado:

{ "ok" : 1 }

6. Defina un esquema para la colección Books e inserte dos documentos con el comando siguiente:

db.Books.insertMany([{'Name':'Design Patterns','Price':54.93,'Category':'Computers','Author':'Ralph
Johnson'}, {'Name':'Clean Code','Price':43.15,'Category':'Computers','Author':'Robert C. Martin'}])

Se muestra el siguiente resultado:

{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5bfd996f7b8e48dc15ff215d"),
ObjectId("5bfd996f7b8e48dc15ff215e")
]
}

7. Vea los documentos en la base de datos mediante el comando siguiente:

db.Books.find({}).pretty()

Se muestra el siguiente resultado:

{
"_id" : ObjectId("5bfd996f7b8e48dc15ff215d"),
"Name" : "Design Patterns",
"Price" : 54.93,
"Category" : "Computers",
"Author" : "Ralph Johnson"
}
{
"_id" : ObjectId("5bfd996f7b8e48dc15ff215e"),
"Name" : "Clean Code",
"Price" : 43.15,
"Category" : "Computers",
"Author" : "Robert C. Martin"
}
El esquema agrega una propiedad _id generada automáticamente del tipo ObjectId para cada
documento.
La base de datos está lista. Puede empezar a crear la API web de ASP.NET Core.

Creación de un proyecto de API web de ASP.NET Core


Visual Studio
Visual Studio Code
Visual Studio para Mac
1. Vaya a Archivo > Nuevo > Proyecto.
2. Seleccione el tipo de proyecto Aplicación web de ASP.NET Core y, luego, Siguiente.
3. Denomine el proyecto BooksApi y seleccione Crear.
4. Seleccione el marco de destino .NET Core y ASP.NET Core 2.2. Seleccione la plantilla de proyecto API y,
luego, Crear.
5. Visite la galería de NuGet: MongoDB.Driver para determinar la última versión estable del controlador .NET
para MongoDB. En la ventana Consola del Administrador de paquetes, desplácese hasta la raíz del
proyecto. Ejecute el siguiente comando para instalar el controlador .NET para MongoDB:

Install-Package MongoDB.Driver -Version {VERSION}

Adición de un modelo de entidad


1. Agregue un directorio Modelos a la raíz del proyecto.
2. Agregue una clase Book al directorio Modelos con el código siguiente:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BooksApi.Models
{
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }

[BsonElement("Name")]
public string BookName { get; set; }

public decimal Price { get; set; }

public string Category { get; set; }

public string Author { get; set; }


}
}

En la clase anterior, se requiere la propiedad Id

para asignar el objeto de Common Language Runtime (CLR ) a la colección de MongoDB.


Se anota con [BsonId] para designar esta propiedad como clave principal del documento.
Se anota con [BsonRepresentation(BsonType.ObjectId)] para permitir que el parámetro pase como tipo
string en lugar de como una estructura ObjectId. Mongo controla la conversión de string a ObjectId .
La propiedad BookName se anota con el atributo [BsonElement]. El valor Name del atributo representa el
nombre de propiedad en la colección de MongoDB.

Adición de un modelo configuración


1. Agregue los siguientes valores de configuración de base de datos a appsettings.json:

{
"BookstoreDatabaseSettings": {
"BooksCollectionName": "Books",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookstoreDb"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

2. Agregue un archivo BookstoreDatabaseSettings.cs al directorio Models con el código siguiente:

namespace BooksApi.Models
{
public class BookstoreDatabaseSettings : IBookstoreDatabaseSettings
{
public string BooksCollectionName { get; set; }
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
}

public interface IBookstoreDatabaseSettings


{
string BooksCollectionName { get; set; }
string ConnectionString { get; set; }
string DatabaseName { get; set; }
}
}

La clase anterior BookstoreDatabaseSettings se utiliza para almacenar los valores de propiedad


BookstoreDatabaseSettings del archivo appsettings.json. Los nombres de las propiedades de JSON y C# son
iguales para facilitar el proceso de asignación.
3. Agregue el código resaltado siguiente a Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

En el código anterior:
La instancia de configuración a la que la sección BookstoreDatabaseSettings del archivo appsettings.json
enlaza está registrada en el contenedor de inserción de dependencias (DI). Por ejemplo, una propiedad
ConnectionString del objeto BookstoreDatabaseSettings se rellena con la propiedad
BookstoreDatabaseSettings:ConnectionString en appsettings.json.
La interfaz IBookstoreDatabaseSettings se registra en la inserción de dependencias con una duración de
servicio de tipo singleton. Cuando se inserta, la instancia de la interfaz se resuelve en un objeto
BookstoreDatabaseSettings .
4. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver las referencias a
BookstoreDatabaseSettings y IBookstoreDatabaseSettings :

using BooksApi.Models;

Adición de un servicio de operaciones CRUD


1. Agregue un directorio Servicios a la raíz del proyecto.
2. Agregue una clase BookService al directorio Servicios con el código siguiente:
using BooksApi.Models;
using MongoDB.Driver;
using System.Collections.Generic;
using System.Linq;

namespace BooksApi.Services
{
public class BookService
{
private readonly IMongoCollection<Book> _books;

public BookService(IBookstoreDatabaseSettings settings)


{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);

_books = database.GetCollection<Book>(settings.BooksCollectionName);
}

public List<Book> Get() =>


_books.Find(book => true).ToList();

public Book Get(string id) =>


_books.Find<Book>(book => book.Id == id).FirstOrDefault();

public Book Create(Book book)


{
_books.InsertOne(book);
return book;
}

public void Update(string id, Book bookIn) =>


_books.ReplaceOne(book => book.Id == id, bookIn);

public void Remove(Book bookIn) =>


_books.DeleteOne(book => book.Id == bookIn.Id);

public void Remove(string id) =>


_books.DeleteOne(book => book.Id == id);
}
}

En el código anterior, se recuperó una instancia de IBookstoreDatabaseSettings de la inserción de


dependencias mediante la inserción de un constructor. Esta técnica proporciona acceso a los valores de
configuración de appsettings.json que se agregaron en la sección Adición de un modelo de configuración.
3. Agregue el código resaltado siguiente a Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddSingleton<BookService>();

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

En el código anterior, la clase BookService se registra con inserción de dependencias para admitir la
inserción del constructor en las clases de consumo. La duración de servicio de tipo singleton es la más
adecuada porque BookService toma una dependencia directa sobre MongoClient . Según las instrucciones
oficiales de reutilización de cliente Mongo, MongoClient debe registrarse en la inserción de dependencias
con una duración de servicio de tipo singleton.
4. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver la referencia a
BookService :

using BooksApi.Services;

La clase BookService usa los miembros MongoDB.Driver siguientes para realizar operaciones CRUD en la base de
datos:
MongoClient: lee la instancia del servidor para realizar operaciones de base de datos. Se proporciona la
cadena de conexión de MongoDB al constructor de esta clase:

public BookService(IBookstoreDatabaseSettings settings)


{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);

_books = database.GetCollection<Book>(settings.BooksCollectionName);
}

IMongoDatabase: representa la base de datos de Mongo para realizar operaciones. Este tutorial usa el
método genérico GetCollection<TDocument>(collection) en la interfaz para tener acceso a los datos de una
colección específica. Realice las operaciones CRUD en la colección después de llamar a este método. En la
llamada al método GetCollection<TDocument>(collection) :
collection representa el nombre de la colección.
TDocument representa el tipo de objeto CLR almacenado en la colección.
GetCollection<TDocument>(collection) devuelve un objeto MongoCollection que representa la colección. En este
tutorial, se invocan los métodos siguientes en la colección:
DeleteOne: elimina un único documento que cumpla los criterios de búsqueda proporcionados.
Find<TDocument>: devuelve todos los documentos de la colección que cumplen los criterios de búsqueda
indicados.
InsertOne: inserta el objeto proporcionado como un nuevo documento en la colección.
ReplaceOne: reemplaza un único documento que cumpla los criterios de búsqueda indicados por el objeto
proporcionado.

Incorporación de un controlador
Agregue una clase BooksController al directorio Controladores con el código siguiente:

using BooksApi.Models;
using BooksApi.Services;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace BooksApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
{
private readonly BookService _bookService;

public BooksController(BookService bookService)


{
_bookService = bookService;
}

[HttpGet]
public ActionResult<List<Book>> Get() =>
_bookService.Get();

[HttpGet("{id:length(24)}", Name = "GetBook")]


public ActionResult<Book> Get(string id)
{
var book = _bookService.Get(id);

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

return book;
}

[HttpPost]
public ActionResult<Book> Create(Book book)
{
_bookService.Create(book);

return CreatedAtRoute("GetBook", new { id = book.Id.ToString() }, book);


}

[HttpPut("{id:length(24)}")]
public IActionResult Update(string id, Book bookIn)
{
var book = _bookService.Get(id);

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

_bookService.Update(id, bookIn);

return NoContent();
}

[HttpDelete("{id:length(24)}")]
public IActionResult Delete(string id)
{
var book = _bookService.Get(id);

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

_bookService.Remove(book.Id);

return NoContent();
}
}
}

El controlador de API web anterior:


Usa la clase BookService para realizar operaciones CRUD.
Contiene métodos de acción para admitir las solicitudes GET, POST, PUT y DELETE de HTTP.
Llama a CreatedAtRoute en el método de acción Create para devolver una respuesta HTTP 201. El código de
estado 201 es la respuesta estándar para un método HTTP POST que crea un recurso en el servidor.
CreatedAtRoute también agrega un encabezado Location a la respuesta. El encabezado Location especifica el
identificador URI del libro recién creado.

Prueba de la API web


1. Compile y ejecute la aplicación.
2. Vaya a http://localhost:<port>/api/books para probar el método de acción Get sin parámetros del
controlador. Se muestra la siguiente respuesta JSON:

[
{
"id":"5bfd996f7b8e48dc15ff215d",
"bookName":"Design Patterns",
"price":54.93,
"category":"Computers",
"author":"Ralph Johnson"
},
{
"id":"5bfd996f7b8e48dc15ff215e",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}
]

3. Vaya a http://localhost:<port>/api/books/5bfd996f7b8e48dc15ff215e para probar el método de acción Get


sobrecargado del controlador. Se muestra la siguiente respuesta JSON:

{
"id":"5bfd996f7b8e48dc15ff215e",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}

Configuración de las opciones de serialización de JSON


Hay dos detalles que cambiar sobre las respuestas JSON devueltas en la sección Prueba de la API web:
Las mayúsculas y minúsculas Camel predeterminadas de los nombres de propiedad se deben cambiar para que
coincidan con el uso de mayúsculas y minúsculas de Pascal de los nombres de propiedad del objeto CLR.
La propiedad bookName se debe devolver como Name .

Para satisfacer los requisitos anteriores, realice los cambios siguientes:


1. En Startup.ConfigureServices , cambie el código resaltado siguiente en la llamada al método AddMvc :
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddSingleton<BookService>();

services.AddMvc()
.AddJsonOptions(options => options.UseMemberCasing())
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Con el cambio anterior, los nombres de propiedad de la respuesta JSON serializada de la API web coinciden
con sus nombres de propiedad correspondientes en el tipo de objeto CLR. Por ejemplo, la propiedad
Author de la clase Book se serializa como Author .

2. En Models/Book.cs, anote la propiedad BookName con el atributo [JsonProperty] siguiente:

[BsonElement("Name")]
[JsonProperty("Name")]
public string BookName { get; set; }

El valor Name del atributo [JsonProperty] representa el nombre de propiedad en la respuesta JSON
serializada de la API web.
3. Agregue el código siguiente en la parte superior del archivo Models/Book.cs para resolver la referencia al
atributo [JsonProperty] :

using Newtonsoft.Json;

4. Repita los pasos definidos en la sección Prueba de la API web. Observe la diferencia en los nombres de
propiedad JSON.

Pasos siguientes
Para obtener más información sobre la creación de las API web de ASP.NET Core, consulte los siguientes recursos:
Versión de YouTube de este artículo
Creación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador de ASP.NET Core Web API
Crear servicios back-end para aplicaciones móviles
nativas con ASP.NET Core
10/05/2019 • 14 minutes to read • Edit Online

Por Steve Smith


Las aplicaciones móviles pueden comunicarse con servicios back-end de ASP.NET Core. Para obtener
instrucciones sobre cómo conectar servicios web locales desde simuladores de iOS y emuladores de Android, vea
Connect to Local Web Services from iOS Simulators and Android Emulators (Conexión a servicios web locales
desde simuladores de iOS y emuladores de Android).
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.

Recursos adicionales
Autenticación y autorización
Tutorial: Introducción a SignalR de ASP.NET Core
04/07/2019 • 12 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:
Cree un proyecto web.
Agregar la biblioteca cliente de SignalR.
Crear un concentrador de SignalR.
Configurar el proyecto para usar SignalR.
Agregar código que envía 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 version 15.9 or later with the ASP.NET and web development workload. You can use
Visual Studio 2019, but some project creation steps differ from what's shown in the tutorial.
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work with
Visual Studio.
Creación de un proyecto web
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.2 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 (CDN ) que puede
entregar todo lo que encuentre en npm, el administrador de paquetes de 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.

Creación de un concentrador de 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.
Puede llamarse al método SendMessage mediante un cliente conectado para enviar un mensaje a todos los
clientes. El código de cliente de JavaScript que llama al método se muestra más adelante en el tutorial. El
código de SignalR es asincrónico para proporcionar la máxima escalabilidad.

Configuración de 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 de ASP.NET Core y a la


canalización de software intermedio.
Adición del 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();

//Disable send button until connection is established


document.getElementById("sendButton").disabled = true;

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().then(function(){
document.getElementById("sendButton").disabled = false;
}).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 mensaje.
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
En este tutorial ha aprendido a:
Crear un proyecto de aplicación web.
Agregar la biblioteca cliente de SignalR.
Crear un concentrador de SignalR.
Configurar el proyecto para usar SignalR.
Agregar código que usa el concentrador para enviar mensajes desde cualquier cliente a todos los clientes
conectados.
Para obtener más información sobre SignalR, vea la introducción:
Introducción a SignalR de ASP.NET Core
Uso de SignalR de ASP.NET Core con TypeScript y
Webpack
21/05/2019 • 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
Visual Studio
Visual Studio Code
Visual Studio 2019 con la carga de trabajo ASP.NET y desarrollo web
.NET Core SDK 2.2 o posterior
Node.js con npm

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


Visual Studio
Visual Studio Code
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.
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 seleccione Aceptar.
3. Seleccione .NET Core en la lista desplegable de plataforma de destino y ASP.NET Core 2.2 en la lista
desplegable del selector de plataforma. Seleccione la plantilla Vacía y 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@1.0.1 css-loader@2.1.0 html-webpack-plugin@4.0.0-beta.5 mini-css-
extract-plugin@0.5.0 ts-loader@5.3.3 typescript@3.3.3 webpack@4.29.3 webpack-cli@3.2.3

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.29.3" en
lugar de "webpack": "^4.29.3" . 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 hace 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(long 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
Visual Studio Code
1. Ejecute Webpack en modo release. Desde la ventana Consola del administrador de paquetes, ejecute el
comando siguiente en la raíz del proyecto. Si no está en la raíz del proyecto, escriba cd SignalRWebPack antes
de introducir el comando.

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
Tutorial: Crear un servidor y un cliente gRPC en
ASP.NET Core
05/07/2019 • 12 minutes to read • Edit Online

Por John Luo


En este tutorial se muestra cómo crear un cliente gRPC de .NET Core y un servidor gRPC de ASP.NET Core.
Al final tendrá un cliente gRPC que se comunica con el servicio Greeter de gRPC.
Vea o descargue el código de ejemplo (cómo descargarlo).
En este tutorial ha:
Crear un servicio gRPC.
Crear un cliente gRPC.
Probar el servicio cliente gRPC con el servicio gRPC Greeter.

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio de 2019 con el ASP.NET y desarrollo web carga de trabajo
Obtener una vista previa de .NET core SDK 3.0

Crear un servicio gRPC


Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
En el cuadro de diálogo Crear un proyecto nuevo, seleccione Aplicación web ASP.NET Core.
Seleccione Siguiente.
Llame al proyecto GrpcGreeter. Es importante asignarle el nombre GrpcGreeter para que los espacios de
nombres coincidan al copiar y pegar el código.
Seleccione Crear.
En el cuadro de diálogo Crear una aplicación web ASP.NET Core:
Seleccione .NET Core y ASP.NET Core 3.0 en los menús desplegables.
Seleccione la plantilla Servicio gRPC.
Seleccione Crear.
Ejecutar el servicio
Visual Studio
Visual Studio Code/Visual Studio para Mac
Presione Ctrl+F5 para ejecutar el servicio gRPC sin el depurador.
Visual Studio ejecuta el servicio en un símbolo del sistema.
Los registros muestran que el servicio está escuchando en http://localhost:50051 .

info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:50051
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]

Examen de los archivo del proyecto


Archivos de proyecto de GrpcGreeter:
greet.proto: El archivo Protos/greet.proto define el gRPC Greeter y se usa para generar los recursos de
servidor gRPC. Para obtener más información, vea Introducción a gRPC.
Carpeta Servicios: contiene la implementación del servicio Greeter .
appSettings.json: contiene datos de configuración, como el protocolo usado por Kestrel. Para más información,
consulte Configuración en ASP.NET Core.
Program.cs: contiene el punto de entrada para el servicio gRPC. Para más información, consulte Host genérico
de .NET.
Startup.cs: Contiene código que configura el comportamiento de la aplicación. Para obtener más información,
vea Inicio de la aplicación.

Creación del cliente gRPC en una aplicación de consola de .NET


Visual Studio
Visual Studio Code
Visual Studio para Mac
Abra una segunda instancia de Visual Studio.
Seleccione Archivo > Nuevo > Proyecto de la barra de menús.
En el cuadro de diálogo Crear un nuevo proyecto, seleccione Aplicación de consola (.NET Core) .
Seleccione Siguiente.
En el cuadro de texto Nombre, escriba "GrpcGreeterClient".
Seleccione Crear.
Adición de paquetes necesarios
El proyecto de cliente gRPC requiere los siguientes paquetes:
Grpc.Net.Client, que contiene el cliente de .NET Core.
Google.Protobuf, que contiene API de mensajes protobuf para C#.
Grpc.Tools, que contiene compatibilidad con herramientas de C# para archivos protobuf. El paquete de
herramientas no es necesario en el runtime, de modo que la dependencia se marca con PrivateAssets="All" .

Visual Studio
Visual Studio Code
Visual Studio para Mac
Instale los paquetes con la Consola del Administrador de paquetes (PMC ) o mediante Administrar paquetes
NuGet.
Opción de PMC para instalar paquetes
En Visual Studio, seleccione Herramientas > Administrador de paquetes de NuGet > Consola del
Administrador de paquetes.
En la ventana de la Consola del Administrador de paquetes, desplácese al directorio en el que se encuentra
el archivo GrpcGreeterClient.csproj.
Ejecute los comandos siguientes:

Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

Administración de la opción Paquetes NuGet para instalar paquetes


Haga clic con el botón derecho en el proyecto en el Explorador de soluciones > Administrar paquetes
NuGet.
Seleccione la pestaña Examinar.
Escriba Grpc.Core en el cuadro de búsqueda.
Seleccione el paquete Grpc.Core en la pestaña Examinar y haga clic en Instalar.
Repita el proceso para Google.Protobuf y Grpc.Tools .
Adición de greet.proto
Cree una carpeta Protos en el proyecto de cliente gRPC.
Copie el archivo Protos\greet.proto del servicio gRPC Greeter en el proyecto de cliente gRPC.
Edite el archivo de proyecto GrpcGreeterClient.csproj:
Visual Studio
Visual Studio Code
Visual Studio para Mac
Haga clic con el botón derecho en el proyecto y seleccione Editar archivo del proyecto.

Agregue un grupo de elementos con un elemento <Protobuf> que hace referencia al archivo greet.proto:

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

Creación del cliente de Greeter


Compile el proyecto para crear los tipos en el espacio de nombres GrpcGreeter . El proceso de compilación
genera automáticamente los tipos GrpcGreeter .
Actualice el archivo Program.cs del cliente gRPC con el código siguiente:
using System;
using System.Net.Http;
using System.Threading.Tasks;
using GrpcGreeter;
using Grpc.Net.Client;

namespace GrpcGreeterClient
{
class Program
{
static async Task Main(string[] args)
{
AppContext.SetSwitch(
"System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
true);
var httpClient = new HttpClient();
// The port number(50051) must match the port of the gRPC server.
httpClient.BaseAddress = new Uri("http://localhost:50051");
var client = GrpcClient.Create<Greeter.GreeterClient>(httpClient);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}

Program.cs contiene el punto de entrada y la lógica para el cliente gRPC.


El cliente de Greeter se crea mediante lo siguiente:
Creación de una instancia de HttpClient que contiene la información para crear la conexión al servicio gRPC.
Uso de HttpClient para construir el cliente de Greeter:

static async Task Main(string[] args)


{
AppContext.SetSwitch(
"System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
true);
var httpClient = new HttpClient();
// The port number(50051) must match the port of the gRPC server.
httpClient.BaseAddress = new Uri("http://localhost:50051");
var client = GrpcClient.Create<Greeter.GreeterClient>(httpClient);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}

El cliente de Greeter realiza una llamada al método SayHello asincrónico. Se muestra el resultado de la llamada a
SayHello :
static async Task Main(string[] args)
{
AppContext.SetSwitch(
"System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
true);
var httpClient = new HttpClient();
// The port number(50051) must match the port of the gRPC server.
httpClient.BaseAddress = new Uri("http://localhost:50051");
var client = GrpcClient.Create<Greeter.GreeterClient>(httpClient);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}

Prueba del cliente gRPC con el servicio gRPC Greeter


Visual Studio
Visual Studio Code/Visual Studio para Mac
En el servicio Greeter, presione Ctrl+F5 para iniciar el servidor sin el depurador.
En el proyecto GrpcGreeterClient , presione Ctrl+F5 para iniciar el servidor sin el depurador.

El cliente envía un saludo al servicio con un mensaje que contiene su nombre "GreeterClient". El servicio envía el
mensaje "Hello GreeterClient" como respuesta. La respuesta "Hello GreeterClient" se muestra en el símbolo del
sistema:

Greeting: Hello GreeterClient


Press any key to exit...

El servicio gRPC registra los detalles de la llamada correcta en los registros escritos en el símbolo del sistema.

info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:50051
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\sample\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/2 POST http://localhost:50051/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 78.32260000000001ms 200 application/grpc

Pasos siguientes
Introducción a gRPC en ASP.NET Core
Servicios gRPC con C#
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
Páginas de Razor de ASP.NET Core con EF Core: serie
de tutoriales
10/05/2019 • 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
17/05/2019 • 29 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear 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/AspNetCore.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 scaffold.
En el cuadro de diálogo Agregar scaffold, seleccione Páginas de Razor 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.Models
{
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();
}
}
}
Nota: El código anterior usa Models para el espacio de nombres ( namespace ContosoUniversity.Models ) en lugar de
Data . Models es coherente con el código generado por el proveedor de scaffolding. Para obtener más
información, consulte este problema de scaffolding de GitHub.
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


El nombre de la base de datos se genera a partir del nombre de contexto proporcionado anteriormente, más un
guión y un GUID. Por lo tanto, el nombre de la base de datos será "SchoolContext-{GUID }". El GUID será diferente
para cada usuario. Abra el Explorador de objetos de SQL Server (SSOX) desde el menú Vista en Visual Studio.
En SSOX, haga clic en (localdb)\MSSQLLocalDB > Databases > SchoolContext-{GUID }.
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.

Recursos adicionales
Versión en YouTube de este tutorial
S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
CRUD (2 de 8)
17/06/2019 • 21 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core y
Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.
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 repositorio de servicio para crear una capa
de abstracción entre la interfaz de usuario (las páginas de Razor) y la capa de acceso a datos.
En este tutorial se examinan las páginas Create, Edit, Delete y Details de Razor Pages de la carpeta Students.
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 Asistente 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:
: 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 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
Students/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
Students/Index no debe contener una plantilla de ruta:

@page "{id:int}"

Cada página de Razor debe incluir la directiva @page .

Recursos adicionales
Versión en YouTube de este tutorial

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)
10/05/2019 • 25 minutes to read • Edit Online

Por Tom Dykstra, Rick Anderson y Jon P Smith


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core y
Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial se agregan las funcionalidades de ordenación, filtrado, agrupación y paginación.
En la siguiente ilustración se muestra una página completa. Los encabezados de columna son vínculos interactivos
para ordenar la columna. Si se hace clic de forma consecutiva en el encabezado de una columna, el criterio de
ordenación cambia entre ascendente y descendente.

Si experimenta problemas que no puede resolver, descargue la aplicación completada.

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


Las plantillas web de ASP.NET Core 2.2 no incluyen la página About. Si usa ASP.NET Core 2.2, cree la página
About de Razor Pages.
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;

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
Versión en YouTube de este tutorial
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)
10/05/2019 • 10 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core y
Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se usa la característica de migraciones de EF Core para administrar cambios en el modelo de datos.
Si experimenta problemas que no puede resolver, descargue la aplicación completada.
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 EnsureCreated :

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
Versión en YouTube de este tutorial
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)
17/05/2019 • 49 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core y
Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En este
tutorial:
Se agregan más entidades y relaciones.
Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base
de datos.
Las clases de entidad para el modelo de datos completo se muestran en la siguiente ilustración:
Si experimenta problemas que no puede resolver, descargue la aplicación completada.

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 del asistente de etiquetas <entrada>.
Ejecutar 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 de las claves externas que no acepten valores
NULL ni 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 la propiedad Department.InstructorID no se ha definido como que acepta valores NULL:
EF Core configura una regla de eliminación en cascada para eliminar el departamento cuando se elimina el
instructor.
Eliminar el departamento cuando se elimine el instructor no es el comportamiento previsto.
La API fluida siguiente establecería una regla de restricción en lugar de en cascada.

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 con 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. Consulte
Enrollments y CourseAssignments para obtener ejemplos de cómo pueden inicializarse las tablas de combinación
de varios a varios.

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'.
Aplicar la migración
Ahora que tiene una base de datos existente, debe pensar cómo aplicar los cambios futuros en ella. En este tutorial
se muestran dos enfoques:
Quitar y volver a crear la base de datos
Aplicar la migración a la base de datos existente. Aunque este método es más complejo y lento, es el método
preferido para entornos de producción del mundo real. Nota: Esta sección del tutorial es opcional. Puede
realizar la operación de quitar y volver a crear, y omitir esta sección. Si quiere seguir los pasos descritos en esta
sección, no realice la operación de quitar y volver a crear.
Quitar y volver a crear 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.


Ejecutar 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.
Aplicar la migración a la base de datos existente
Esta sección es opcional. Estos pasos solo funcionan si pasó por alto la sección Quitar y volver a crear la base de
datos.
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" :

migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);

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.

Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
10/05/2019 • 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 o vea la aplicación completada. Instrucciones de
descarga.
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. Se ha agregado la carga diferida a EF Core en la versión 2.1. 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="Create">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.

Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
10/05/2019 • 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 o vea la aplicación completada. Instrucciones de descarga.
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 Asistente 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 un asistente 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.
Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
11/06/2019 • 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 o vea la aplicación completada.
Instrucciones de descarga.

Conflictos de simultaneidad
Un conflicto de simultaneidad se produce cuando:
Un usuario va a la página de edición de una entidad.
Otro usuario actualiza la misma entidad antes de que el cambio del primer usuario se escriba en la base de
datos.
Si no está habilitada la detección de simultaneidad, cuando se produzcan actualizaciones simultáneas:
Prevalece la última actualización. Es decir, los últimos valores de actualización se guardan en la base de datos.
La primera de las actualizaciones actuales se pierde.
Simultaneidad optimista
La simultaneidad optimista permite que se produzcan conflictos de simultaneidad y luego reacciona correctamente
si ocurren. Por ejemplo, Jane visita la página de edición de Department y cambia el presupuesto para el
departamento de inglés de 350.000,00 a 0,00 USD.
Antes de que Jane haga clic en Save, John visita la misma página y cambia el campo Start Date de 9/1/2007 a
9/1/2013.
Jane hace clic en Save primero y ve su cambio cuando el explorador muestra la página de índice.

John hace clic en Save en una página Edit que sigue mostrando un presupuesto de 350.000,00 USD. Lo que
sucede después viene determinado por cómo controla los conflictos de simultaneidad.
La simultaneidad optimista incluye las siguientes opciones:
Puede realizar un seguimiento de la propiedad que ha modificado un usuario y actualizar solo las columnas
correspondientes de la base de datos.
En el escenario, no se perderá ningún dato. Los dos usuarios actualizaron diferentes propiedades. La
próxima vez que un usuario examine el departamento de inglés, verá los cambios tanto de Jane como de
John. Este método de actualización puede reducir el número de conflictos que pueden dar lugar a una
pérdida de datos. Este enfoque:
No puede evitar la pérdida de datos si se realizan cambios paralelos a la misma propiedad.
Por lo general, no es práctico en una aplicación web. Requiere mantener un estado significativo para
realizar un seguimiento de todos los valores capturados y nuevos. El mantenimiento de grandes
cantidades de estado puede afectar al rendimiento de la aplicación.
Puede aumentar la complejidad de las aplicaciones en comparación con la detección de simultaneidad en
una entidad.
Puede permitir que los cambios de John sobrescriban los cambios de Jane.
La próxima vez que un usuario examine el departamento de inglés, verá 9/1/2013 y el valor de 350.000,00
USD capturado. Este enfoque se denomina un escenario de Prevalece el cliente o Prevalece el último. (Todos
los valores del cliente tienen prioridad sobre lo que aparece en el almacén de datos). Si no hace ninguna
codificación para el control de la simultaneidad, Prevalece el cliente se realizará automáticamente.
Puede evitar que el cambio de John se actualice en la base de datos. Normalmente, la aplicación podría:
Mostrar un mensaje de error.
Mostrar el estado actual de los datos.
Permitir al usuario volver a aplicar los cambios.
Esto se denomina un escenario de Prevalece el almacén. (Los valores del almacén de datos tienen prioridad
sobre los valores enviados por el cliente). En este tutorial implementará el escenario de Prevalece el almacén.
Este método garantiza que ningún cambio se sobrescriba sin que se avise al usuario.

Administrar la simultaneidad
Cuando una propiedad se configura como un token de simultaneidad:
EF Core comprueba que no se ha modificado la propiedad después de que se capturase. La comprobación se
produce cuando se llama a SaveChanges o SaveChangesAsync.
Si se ha cambiado la propiedad después de haberla capturado, se produce una excepción
DbUpdateConcurrencyException.
Deben configurarse el modelo de datos y la base de datos para que admitan producir una excepción
DbUpdateConcurrencyException .

Detectar conflictos de simultaneidad en una propiedad


Se pueden detectar conflictos de simultaneidad en el nivel de propiedad con el atributo ConcurrencyCheck. El
atributo se puede aplicar a varias propiedades en el modelo. Para obtener más información, consulte Anotaciones
de datos: ConcurrencyCheck.
El atributo [ConcurrencyCheck] no se usa en este tutorial.
Detectar conflictos de simultaneidad en una fila
Para detectar conflictos de simultaneidad, se agrega al modelo una columna de seguimiento rowversion.
rowversion :

Es específico de SQL Server. Otras bases de datos podrían no proporcionar una característica similar.
Se usa para determinar que no se ha cambiado una entidad desde que se capturó de la base de datos.
La base de datos genera un número rowversion secuencial que se incrementa cada vez que se actualiza la fila. En
un comando Update o Delete , la cláusula Where incluye el valor capturado de rowversion . Si la fila que se está
actualizando ha cambiado:
rowversionno coincide con el valor capturado.
Los comandos Update o Delete no encuentran una fila porque la cláusula Where incluye la rowversion
capturada.
Se produce una excepción DbUpdateConcurrencyException .

En EF Core, cuando un comando Update o Delete no han actualizado ninguna fila, se produce una excepción de
simultaneidad.
Agregar una propiedad de seguimiento a la entidad Department
En Models/Department.cs, agregue una propiedad de seguimiento denominada RowVersion:

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

namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }

[StringLength(50, MinimumLength = 3)]


public string Name { get; set; }

[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }

public int? InstructorID { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }

public Instructor Administrator { get; set; }


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

El atributo Timestamp especifica que esta columna se incluye en la cláusula Where de los comandos Update y
Delete . El atributo se denomina Timestamp porque las versiones anteriores de SQL Server usaban un tipo de
datos timestamp antes de que el tipo rowversion de SQL lo sustituyera por otro.
La API fluida también puede especificar la propiedad de seguimiento:

modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();

El código siguiente muestra una parte del T-SQL generado por EF Core cuando se actualiza el nombre de
Department:

SET NOCOUNT ON;


UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
El código resaltado anteriormente muestra la cláusula WHERE que contiene RowVersion . Si la base de datos
RowVersion no es igual al parámetro RowVersion ( @p2 ), no se ha actualizado ninguna fila.

El código resaltado a continuación muestra el T-SQL que comprueba que se actualizó exactamente una fila:

SET NOCOUNT ON;


UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

@@ROWCOUNT devuelve el número de filas afectadas por la última instrucción. Si no se actualiza ninguna fila, EF
Core produce una excepción DbUpdateConcurrencyException .
Puede ver el T-SQL que genera EF Core en la ventana de salida de Visual Studio.
Actualizar la base de datos
Agregar la propiedad RowVersion cambia el modelo de base de datos, lo que requiere una migración.
Compile el proyecto. Escriba lo siguiente en una ventana de comandos:

dotnet ef migrations add RowVersion


dotnet ef database update

Los comandos anteriores:


Agregan el archivo de migración Migrations/{time stamp }_RowVersion.cs.
Actualizan el archivo Migrations/SchoolContextModelSnapshot.cs. La actualización agrega el siguiente
código resaltado al método BuildModel :

modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
{
b.Property<int>("DepartmentID")
.ValueGeneratedOnAdd();

b.Property<decimal>("Budget")
.HasColumnType("money");

b.Property<int?>("InstructorID");

b.Property<string>("Name")
.HasMaxLength(50);

b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();

b.Property<DateTime>("StartDate");

b.HasKey("DepartmentID");

b.HasIndex("InstructorID");

b.ToTable("Department");
});

Ejecutan las migraciones para actualizar la base de datos.


Aplicar la técnica scaffolding al modelo Departments
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 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 IActionResult HandleDeletedDepartment()


{
var 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 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 publicado 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
Versión de YouTube de este tutorial (gestión de conflictos de simultaneidad)
Versión en YouTube de este tutorial (parte 2)
Versión en YouTube de este tutorial (parte 3)

A N T E R IO R
ASP.NET Core MVC con EF Core: serie de tutoriales
10/05/2019 • 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.
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
Tutorial: Introducción a EF Core en una aplicación
web de ASP.NET Core MVC
21/05/2019 • 40 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.
En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET Core 2.2
MVC con Entity Framework (EF ) Core 2.2 y Visual Studio 2017 o 2019.
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.
En este tutorial ha:
Creación de una aplicación web de ASP.NET Core MVC
Configurar el estilo del sitio
Obtiene información sobre los paquetes NuGet de EF Core
Crear el modelo de datos
Crear el contexto de base de datos
Registrar el contexto para la inserción de dependencias
Inicializar la base de datos con datos de prueba
Crear un controlador y vistas
Consulta la base de datos

Requisitos previos
SDK de .NET Core 2.2
Visual Studio 2019 con las cargas de trabajo siguientes:
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core

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.

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.
Creación de una aplicación web
Abra Visual Studio.
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.
Seleccione .NET Core, ASP.NET Core 2.2 y la plantilla Aplicación web (controlador de vista de
modelos).
Asegúrese de que Autenticación esté establecida en Sin autenticación.
Seleccione 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 About, Students, Courses, Instructors y Departments, y elimine la
entrada de menú Privacy.
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 include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/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"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow
mb-3">
mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso
University</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-
collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="About">About</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-
action="Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-
action="Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-
action="Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-
action="Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2019 - Contoso University - <a asp-area="" asp-controller="Home" asp-
action="Privacy">Privacy</a>
</div>
</footer>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

@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/AspNetCore.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.
Acerca de los paquetes NuGet de EF 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.App,
por lo que no es necesario hacer referencia al paquete.
Este paquete de SQL Server de EF 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");
}
}
}

Registra SchoolContext
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.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

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;
using Microsoft.AspNetCore.Http;

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

Inicializa 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=2021,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 = CreateWebHostBuilder(args).Build();

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.

Crea 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.
En el cuadro de diálogo Agregar scaffold:
Seleccione Controlador de MVC con vistas que usan Entity Framework.
Haga clic en Agregar. Aparece el cuadro de diálogo Agregar un controlador de MVC con vistas
que usan Entity Framework.

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 Students 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.
Consulta 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\<su_nombre_de_usuario>.


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 <nombre de la propiedad de
navegación><nombre de la propiedad de clave principal (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 <nombre de la propiedad de clave principal> (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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Creado una aplicación web de ASP.NET Core MVC
Configurar el estilo del sitio
Obtenido información sobre los paquetes NuGet de EF Core
Creado el modelo de datos
Creado el contexto de la base de datos
Registrado SchoolContext
Inicializado la base de datos con datos de prueba
Creado un controlador y vistas
Consultado la base de datos
En el tutorial siguiente, obtendrá información sobre cómo realizar operaciones CRUD (crear, leer, actualizar y
eliminar) básicas.
Pase al tutorial siguiente para obtener información sobre cómo realizar operaciones CRUD (crear, leer, actualizar y
eliminar) básicas.
Implementación de la funcionalidad CRUD básica
Tutorial: Implementación de la funcionalidad CRUD:
ASP.NET MVC con EF Core
17/06/2019 • 38 minutes to read • Edit Online

En el tutorial anterior, creó una aplicación MVC que almacena y muestra los datos con Entity Framework y SQL
Server LocalDB. En este tutorial, podrá revisar y personalizar el código CRUD (crear, leer, actualizar y eliminar) que
el scaffolding de MVC crea automáticamente para usted en controladores y vistas.

NOTE
Es una práctica habitual implementar el modelo de repositorio con el fin de crear una capa de abstracción entre el controlador
y la capa de acceso a datos. Para que estos tutoriales sean sencillos y se centren en enseñar a usar Entity Framework, no se
usan repositorios. Para obtener información sobre los repositorios con EF, vea el último tutorial de esta serie.

En este tutorial ha:


Personalizar la página de detalles
Actualizar la página Create
Actualizar la página Edit
Actualizar la página Delete
Cerrar conexiones de bases de datos

Requisitos previos
Introducción a EF Core y ASP.NET Core MVC

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()
.FirstOrDefaultAsync(m => m.ID == id);

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

return View(student);
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación
Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course . Obtendrá más
información sobre estos métodos en el tutorial de lectura de datos relacionados.
El método AsNoTracking mejora el rendimiento en casos en los que no se actualizarán las entidades devueltas en la
duración del contexto actual. Obtendrá más información sobre AsNoTracking al final de este tutorial.
Datos de ruta
El valor de clave que se pasa al método Details procede de los datos de ruta. Los datos de ruta son los que el
enlazador de modelos encuentra en un segmento de la dirección URL. Por ejemplo, la ruta predeterminada
especifica los segmentos de controlador, acción e identificador:

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

En la dirección URL siguiente, la ruta predeterminada asigna Instructor como el controlador, Index como la acción y
1 como el identificador; estos son los valores de datos de ruta.

http://localhost:1230/Instructor/Index/1?courseID=2021

La última parte de la dirección URL ("?courseID=2021") es un valor de cadena de consulta. El enlazador de


modelos también pasará el valor ID al parámetro id del método Details si se pasa como un valor de cadena de
consulta:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

En la página Index, las instrucciones del asistente 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 los asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.
Agregar inscripciones a la vista de detalles
Abra Views/Students/Details.cshtml. Cada campo se muestra mediante los asistentes DisplayNameFor y DisplayFor
, como se muestra en el ejemplo siguiente:

<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd class="col-sm-10">
@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 class="col-sm-2">
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
<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 los asistentes 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.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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.FindAsync(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).

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

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

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado la página de detalles
Actualizado la página Create
Actualizado la página Edit
Actualizado la página Delete
Cerrado conexiones de bases de datos
Pase al tutorial siguiente para obtener información sobre cómo expandir la funcionalidad de la página Index
mediante la adición de ordenación, filtrado y paginación.
Siguiente: Ordenación, filtrado y paginación
Tutorial: Adición de ordenación, filtrado y paginación:
ASP.NET MVC con EF Core
17/05/2019 • 25 minutes to read • Edit Online

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.

En este tutorial ha:


Agrega vínculos de ordenación de columnas
Agrega un cuadro de búsqueda
Agrega paginación al índice de Students
Agrega paginación al método Index
Agrega vínculos de paginación
Crea una página About

Requisitos previos
Implementación de la funcionalidad CRUD

Agrega vínculos de ordenación de columnas


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.
Agrega un cuadro de búsqueda
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 llamar al método ToUpper para hacer explícitamente que la prueba no distinga entre
mayúsculas y minúsculas: Where(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 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, 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.

Agrega paginación al í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.

Agrega 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? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

if (searchString != null)
{
pageNumber = 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(), pageNumber ?? 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? pageNumber)

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)
{
pageNumber = 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(), pageNumber ?? 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 (pageNumber ?? 1) devuelve el valor de pageNumber si tiene
algún valor o devuelve 1 si pageNumber es NULL.

Agrega vínculos de paginación


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-pageNumber="@(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-pageNumber="@(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 asistentes de etiquetas:


<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(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.

Crea una página About


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.
Cree el método About en el controlador Home.
Cree 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;
}

Agregue un método About en 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 .

Creación de la vista About


Agregue un archivo Views/Home/About.cshtml con 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.

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Agregado vínculos de ordenación de columnas
Agregado un cuadro de búsqueda
Agregado paginación al índice de Students
Agregado paginación al método Index
Agregado vínculos de paginación
Creado una página About
Pase al tutorial siguiente para obtener información sobre cómo controlar los cambios en el modelo de datos
mediante migraciones.
Siguiente: Control de los cambios en el modelo de datos
Tutorial: Uso de la característica de migraciones:
ASP.NET MVC con EF Core
10/05/2019 • 13 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre las migraciones
Cambiar la cadena de conexión
Crear una migración inicial
Examina los métodos Up y Down
Obtiene información sobre la instantánea del modelo de datos
Aplicar la migración

Requisitos previos
Ordenar, filtrar y paginar

Acerca de 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.
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.

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 la carpeta 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.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.2.0-rtm-35687 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.

Examina 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
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 Registro en ASP.NET Core.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.2.0-rtm-35687 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (274ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (60ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [ContosoUniversity2] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [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[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190327172701_InitialCreate', N'2.2.0-rtm-35687');
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.

Comparar CLI y 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 se incluye en el metapaquete Microsoft.AspNetCore.App, por lo que no es necesario agregar una
referencia de paquete si la aplicación tiene una para Microsoft.AspNetCore.App .
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).

Obtención del código


Descargue o vea la aplicación completa.

Paso siguiente
En este tutorial ha:
Obtenido información sobre las migraciones
Obtenido información sobre los paquetes de migración de NuGet
Cambiado la cadena de conexión
Creado una migración inicial
Examinado los métodos Up y Down
Obtenido información sobre la instantánea del modelo de datos
Aplicado la migración
Pase al tutorial siguiente para comenzar a examinar temas más avanzados sobre la expansión del modelo de datos.
Por el camino, podrá crear y aplicar migraciones adicionales.
Creación y aplicación de migraciones adicionales
Tutorial: Creación de un modelo de datos complejo:
ASP.NET MVC con EF Core
17/05/2019 • 53 minutes to read • Edit Online

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:

En este tutorial ha:


Personaliza el modelo de datos
Realiza cambios a la entidad Student
Crea la entidad Instructor
Crea la entidad OfficeAssignment
Modifica la entidad Course
Crea la entidad Department
Modifica la entidad Enrollment
Actualizar el contexto de base de datos
Inicializa la base de datos con datos de prueba
Agregar una migración
Cambiar la cadena de conexión
Actualizar la base de datos

Requisitos previos
Uso de migraciones de EF Core

Personaliza el modelo de datos


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)]
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) e intente escribir cualquier
nombre de más de 50 caracteres. La aplicación debería impedir que lo haga.
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)]
[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 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)]
[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.

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

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

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

Crea 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 departamento cuando se
elimine el instructor, 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)

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

Acerca de una alternativa de la API fluida


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.

Inicializa 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;
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 },
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
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};

foreach (CourseAssignment ci in courseInstructors)


{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();

var enrollments = new Enrollment[]


{
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alexander").ID,
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
Grade = Grade.B
},
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Anand").ID,
CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title == "Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Justice").ID,
CourseID = courses.Single(c => c.Title == "Literature").CourseID,
Grade = Grade.B
}
};

foreach (Enrollment e in enrollments)


{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID == e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}

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


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

Actualizar la base de datos


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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado el modelo de datos
Realizado cambios a la entidad Student
Creado la entidad Instructor
Creado la entidad OfficeAssignment
Modificado la entidad Course
Creado la entidad Department
Modificado la entidad Enrollment
Actualizado el contexto de base de datos
Inicializado la base de datos con datos de prueba
Agregado una migración
Cambiado la cadena de conexión
Actualizado la base de datos
Pase al artículo siguiente para más información sobre cómo acceder a datos relacionados.
Siguiente: Acceso a datos relacionados
Tutorial: Lectura de datos relacionados: ASP.NET MVC
con EF Core
10/05/2019 • 26 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre cómo cargar datos relacionados
Crea una página de cursos
Crea una página de instructores
Obtiene información sobre la carga explícita

Requisitos previos
Creación de un modelo de datos complejo

Obtiene información sobre cómo cargar 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.

Crea una página de cursos


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.

Crea una página de instructores


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.

Acerca de la 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.

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Obtenido información sobre cómo cargar datos relacionados
Creado una página de cursos
Creado una página de instructores
Obtenido información sobre la carga explícita
Pase al tutorial siguiente para obtener información sobre cómo actualizar datos relacionados.
Actualización de datos relacionados
Tutorial: Actualización de datos relacionados: ASP.NET
MVC con EF Core
10/05/2019 • 31 minutes to read • Edit Online

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.
En este tutorial ha:
Personaliza las páginas de cursos
Agrega la página de edición de instructores
Agrega cursos a la página de edición
Actualiza la página Delete
Agrega la ubicación de la oficina y cursos a la página Create

Requisitos previos
Lectura de datos relacionados

Personaliza las páginas de cursos


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


.FirstOrDefaultAsync(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"
al asistente de etiquetas <select> , y luego el asistente 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()
.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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 un asistente 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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd class="col-sm-10">
@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.

Agrega la página de edición de instructores


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()
.FirstOrDefaultAsync(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)
.FirstOrDefaultAsync(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.
Agrega cursos a la página de edición
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()
.FirstOrDefaultAsync(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)
.FirstOrDefaultAsync(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.FirstOrDefault(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.FirstOrDefault(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.FirstOrDefault(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.FirstOrDefault(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, los saltos de línea podrían cambiarse de tal forma que el código se interrumpiese. Si el
código tiene un aspecto diferente después de pegarlo, 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. Este problema se ha corregido en Visual Studio 2019.
<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.

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

Agrega 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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado las páginas de cursos
Agregado la página de edición de instructores
Agregado cursos a la página de edición
Actualizado la página Delete
Agregado la ubicación de la oficina y cursos a la página Create
Pase al tutorial siguiente para obtener información sobre cómo controlar conflictos de simultaneidad.
Control de conflictos de simultaneidad
Tutorial: Control de simultaneidad: ASP.NET MVC con
EF Core
20/06/2019 • 34 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre los conflictos de simultaneidad
Agrega una propiedad de seguimiento
Crea un controlador y vistas de Departments
Actualiza la vista de índice
Actualiza los métodos de edición
Actualiza la vista de edición
Prueba los conflictos de simultaneidad
Actualizar la página Delete
Actualizar las vistas Details y Create

Requisitos previos
Actualización de datos relacionados

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.

Agrega una propiedad de seguimiento


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

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

Actualiza la vista de índice


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.

Actualiza los métodos de edición


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

Actualiza la vista de edición


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

Prueba los conflictos de simultaneidad


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()
.FirstOrDefaultAsync(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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@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");}
}

Obtención del código


Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre cómo administrar la simultaneidad en EF Core, vea Concurrency conflicts
(Conflictos de simultaneidad).

Pasos siguientes
En este tutorial ha:
Obtenido información sobre los conflictos de simultaneidad
Agregado una propiedad de seguimiento
Creado un controlador y vistas de Departments
Actualizado la vista de índice
Actualizado los métodos de edición
Actualizado la vista de edición
Probado los conflictos de simultaneidad
Actualizado la página Delete
Actualizado las vistas Details y Create
Pase al tutorial siguiente para obtener información sobre cómo implementar la herencia de tabla por jerarquía para
las entidades Instructor y Student.
Siguiente: Implementación de la herencia de tabla por jerarquía
Tutorial: Implementación de la herencia: ASP.NET
MVC con EF Core
10/05/2019 • 14 minutes to read • Edit Online

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.
En este tutorial ha:
Asigna la herencia a la base de datos
Creación de la clase Person
Actualiza Instructor y Student
Agrega Person al modelo
Crea y actualiza migraciones
Prueba la implementación

Requisitos previos
Control de la simultaneidad

Asigna la herencia a la base 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;
}
}
}
}

Actualiza Instructor y Student


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


}
}

Agrega Person al modelo


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.

Crea y actualiza migraciones


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 la implementación
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.
Obtención del código
Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre la herencia en Entity Framework Core, consulte Herencia.

Pasos siguientes
En este tutorial ha:
Asignado la herencia a la base de datos
Creado la clase Person
Actualizado Instructor y Student
Agregado Person al modelo
Creado y actualizado migraciones
Probado la implementación
Pase al tutorial siguiente para obtener información sobre cómo controlar una serie de escenarios de Entity
Framework relativamente avanzados.
Siguiente: Temas avanzados
Tutorial: Información sobre escenarios avanzados:
ASP.NET MVC con EF Core
10/05/2019 • 25 minutes to read • Edit Online

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.
En este tutorial ha:
Realiza consultas SQL sin formato
Llama a una consulta para devolver entidades
Llama a una consulta para devolver otros tipos
Llamar a una consulta update
Examina consultas SQL
Crea una capa de abstracción
Obtiene información sobre la detección de cambios automática
Obtiene información sobre el código fuente y planes de desarrollo de Entity Framework Core
Obtiene información sobre cómo usar LINQ dinámico para simplificar el código

Requisitos previos
Implementación de la herencia

Realiza 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.
Llama a una consulta para devolver 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()
.FirstOrDefaultAsync();

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.

Llama a una consulta para devolver 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 Core en Instalado en el panel
izquierdo, haga clic en Vista de Razor 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.

Examina consultas SQL


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.

Crea una capa de abstracción


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 el código


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? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

if (searchString != null)
{
pageNumber = 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(),
pageNumber ?? 1, pageSize));
}

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. John Parente y Paul Goldman trabajaron
en la actualización del tutorial de ASP.NET Core 2.2.

Solucionar problemas de 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
SQL, error: 26: error al buscar el servidor o la instancia especificados)
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.

Obtención del código


Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre EF Core, consulte la documentación de Entity Framework Core. También hay
disponible un libro: Entity Framework Core en acción.
Para obtener información sobre cómo implementar una aplicación web, consulte Hospedaje e implementación de
ASP.NET Core.
Para obtener información sobre otros temas relacionados con ASP.NET Core MVC, como la autenticación y
autorización, vea Introducción a ASP.NET Core.

Pasos siguientes
En este tutorial ha:
Realizado consultas SQL sin formato
Llamado a una consulta para devolver entidades
Llamado a una consulta para devolver otros tipos
Llamado a una consulta update
Examinado consultas SQL
Creado una capa de abstracción
Obtenido información sobre la detección de cambios automática
Obtenido información sobre el código fuente y planes de desarrollo de Entity Framework Core
Obtenido información sobre cómo usar LINQ dinámico para simplificar el código
Con esto finaliza esta serie de tutoriales sobre cómo usar Entity Framework Core en una aplicación ASP.NET Core
MVC. En esta serie se ha trabajado con una base de datos nueva, pero también se pueden utilizar técnicas de
ingeniería inversa en un modelo de una base de datos existente.
Tutorial: EF Core con MVC, base de datos existente
Conceptos básicos de ASP.NET Core
17/06/2019 • 19 minutes to read • Edit Online

Este artículo es una introducción de los temas clave para entender cómo desarrollar aplicaciones de ASP.NET
Core.

Clase Startup
La clase Startup es donde:
Se configuran los servicios requeridos por la aplicación.
Se define la solicitud de canalización.
Los servicios son componentes que usan la aplicación. Por ejemplo, un componente de registro es un servicio. Se
agrega al método Startup.ConfigureServices el código para configurar (o registrar) servicios.
La canalización de control de solicitudes se compone de una serie de componentes de software intermedio. Por
ejemplo, un software intermedio podría controlar las solicitudes de archivos estáticos o redirigir las solicitudes
HTTP a HTTPS. Cada software intermedio lleva a cabo las operaciones asincrónicas en un contexto HttpContext y,
después, invoca el siguiente software intermedio de la canalización o finaliza la solicitud. Se agrega al método
Startup.Configure el código para configurar la canalización de control de solicitudes.

Aquí tiene una clase Startup de ejemplo:

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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

public void Configure(IApplicationBuilder app)


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

Para obtener más información, vea Inicio de la aplicación en ASP.NET Core.

Inserción de dependencias (servicios)


ASP.NET Core tiene un marco de inserción de dependencias (DI) integrado que pone a disposición los servicios
configurados para las clases de una aplicación. Una manera de obtener una instancia de un servicio en una clase
es crear un constructor con un parámetro del tipo necesario. El parámetro puede ser el tipo de servicio o una
interfaz. El sistema de DI proporciona el servicio en tiempo de ejecución.
Aquí tiene una clase que usa DI para obtener un objeto de contexto de Entity Framework Core. La línea resaltada
es un ejemplo de inserción de constructor:
public class IndexModel : PageModel
{
private readonly RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovieContext context)


{
_context = context;
}
// ...
public async Task OnGetAsync()
{
var movies = from m in _context.Movies
select m;
Movies = await movies.ToListAsync();
}
}

Aunque DI está integrada, está diseñada para permitirle conectar un contenedor de inversión de control (IoC ) de
terceros si lo prefiere.
Para obtener más información, vea Inserción de dependencias en ASP.NET Core.

Software intermedio
La canalización de control de solicitudes se compone de una serie de componentes de software intermedio. Cada
componente lleva a cabo las operaciones asincrónicas en un contexto 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 a la canalización al invocar su método de
extensión Use... en el método Startup.Configure . Por ejemplo, para habilitar la representación de los archivos
estáticos, llame a UseStaticFiles .
El código resaltado en el ejemplo siguiente configura la canalización de control de solicitudes:

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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

public void Configure(IApplicationBuilder app)


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

ASP.NET Core incluye una gran variedad de software intermedio integrado. Además, puede escribir software
intermedio personalizado.
Para obtener más información, vea Middleware de ASP.NET Core.

administrador de flujos de trabajo


Una aplicación de ASP.NET Core compila un host durante el inicio. El host es un objeto que encapsula todos los
recursos de la aplicación, como:
Una implementación de servidor de HTTP
Componentes de software intermedio
Registro
DI
Configuración
La razón principal para incluir todos los recursos interdependientes de la aplicación en un objeto es la
administración de la duración: el control sobre el inicio de la aplicación y el apagado estable.
Hay dos hosts disponibles: el host genérico y el host web. Se recomienda el host genérico, mientras que el host
web está disponible solo para compatibilidad con versiones anteriores.
El código para crear un host está en Program.Main :

public class Program


{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

Los métodos CreateDefaultBuilder y ConfigureWebHostDefaults configuran un host con opciones de uso común,
como las siguientes:
Use Kestrel como servidor web y habilite la integración de IIS.
Cargue la configuración de appsettings.json, appsettings.[nombre del entorno ].json, las variables de entorno, los
argumentos de línea de comandos y otros orígenes de configuración.
Envíe la salida de registro a la consola y los proveedores de depuración.
Para obtener más información, vea Host genérico de .NET.
Hay dos hosts disponibles: el host web y el host genérico. En ASP.NET Core 2.x, el host genérico es solo para
escenarios que no son web.
El código para crear un host está en Program.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étodos CreateDefaultBuilder configura un host con opciones de uso común, como las siguientes:
Use Kestrel como servidor web y habilite la integración de IIS.
Cargue la configuración de appsettings.json, appsettings.[nombre del entorno ].json, las variables de entorno, los
argumentos de línea de comandos y otros orígenes de configuración.
Envíe la salida de registro a la consola y los proveedores de depuración.
Para obtener más información, vea Host web de ASP.NET Core.
Escenarios que no son web
El host genérico permite que otros tipos de aplicaciones usen las extensiones de marcos transversales, como el
registro, la inserción de dependencias (DI), la configuración y la administración de la duración de la aplicación.
Para obtener más información, vea Host genérico de .NET y Tareas en segundo plano con servicios hospedados en
ASP.NET Core.

Servidores
Una aplicación ASP.NET Core usa una implementación de servidor HTTP para escuchar las solicitudes HTTP. El
servidor expone las solicitudes a la aplicación como un conjunto de características de solicitud integradas en un
contexto HttpContext .
Windows
macOS
Linux
ASP.NET Core proporciona las siguientes implementaciones de servidor:
Kestrel es un servidor web multiplataforma. Kestrel se suele ejecutar en una configuración de proxy inverso con
IIS. En ASP.NET Core 2.0 y versiones posteriores, Kestrel puede ejecutarse como servidor perimetral de acceso
público expuesto directamente a Internet.
Un servidor HTTP de IIS es un servidor para Windows que usa IIS. Con este servidor, la aplicación de ASP.NET
Core e IIS se ejecutan en el mismo proceso.
HTTP.sys es un servidor de Windows que no se usa con IIS.
Windows
macOS
Linux
ASP.NET Core proporciona las siguientes implementaciones de servidor:
Kestrel es un servidor web multiplataforma. Kestrel se suele ejecutar en una configuración de proxy inverso con
IIS. En ASP.NET Core 2.0 y versiones posteriores, Kestrel puede ejecutarse como servidor perimetral de acceso
público expuesto directamente a Internet.
HTTP.sys es un servidor de Windows que no se usa con IIS.
Para obtener más información, vea Implementaciones de servidores web en ASP.NET Core.

Configuración
ASP.NET Core proporciona un marco de configuración que obtiene la configuración como pares nombre/valor de
un conjunto ordenado de proveedores de configuración. Hay proveedores de configuración integrados para una
gran variedad de orígenes, como archivos .json y .xml, variables de entorno y argumentos de línea de comandos.
También puede escribir proveedores de configuración personalizados.
Por ejemplo, puede especificar que la configuración procede de appsettings.json y las variables de entorno.
Después, cuando se solicite el valor de ConnectionString, el marco buscará primero en el archivo appsettings.json.
Si se encuentra el valor allí, pero también en una variable de entorno, el valor de la variable de entorno tendría
prioridad.
Para administrar los datos de configuración confidencial como las contraseñas, ASP.NET Core proporciona una
herramienta de administrador secreto. Para los secretos de producción, se recomienda Azure Key Vault.
Para obtener más información, vea Configuración en ASP.NET Core.

Opciones
Siempre que sea posible, ASP.NET Core sigue el patrón de opciones para almacenar y recuperar los valores de
configuración. El patrón de opciones usa clases para representar grupos de configuraciones relacionadas.
Por ejemplo, el código siguiente define las opciones de WebSockets:

var options = new WebSocketOptions


{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4096
};
app.UseWebSockets(options);

Para obtener más información, vea Patrón de opciones en ASP.NET Core.

Entornos
Los entornos de ejecución, como desarrollo, almacenamiento provisional y Producción, son un concepto de
primera clase en ASP.NET Core. Puede especificar el entorno que ejecuta una aplicación estableciendo la variable
de entorno ASPNETCORE_ENVIRONMENT . ASP.NET Core lee dicha variable de entorno al inicio de la aplicación y
almacena el valor en una implementación IHostingEnvironment . El objeto de entorno está disponible en cualquier
parte de la aplicación a través de DI.
El siguiente ejemplo de código desde la clase Startup configura la aplicación para que proporcione información
detallada del error solo cuando se ejecuta en el desarrollo:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc();
}

Para obtener más información, vea Usar varios entornos 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 integrados y de terceros. Entre los proveedores disponibles se incluyen los siguientes:
Consola
Depuración
Seguimiento de eventos en Windows
Registro de errores de Windows
TraceSource
Azure App Service
Azure Application Insights
Escribir registros desde cualquier lugar en el código de una aplicación mediante la obtención de un objeto
ILogger de DI y llamar a métodos de registro.

Aquí tiene un código de ejemplo que usa un objeto ILogger , con la inserción del constructor y resaltadas las
llamadas del método de registro.

public class TodoController : ControllerBase


{
private readonly ILogger _logger;

public TodoController(ILogger<TodoController> logger)


{
_logger = logger;
}

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


public ActionResult<TodoItem> GetById(string id)
{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
// Item lookup code removed.
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return item;
}
}

La interfaz ILogger le permite pasar cualquier número de campos para el proveedor de registro. Los campos
habitualmente se usan para construir una cadena de mensaje, pero el proveedor también puede enviarlos como
campos independientes o almacén de datos. Esta característica permite a los proveedores de registro implementar
el registro semántico, también conocido como registro estructurado.
Para obtener más información, vea Registro en ASP.NET Core.

Enrutamiento
Una ruta es un patrón de dirección URL que se asigna a un controlador. El controlador normalmente es una
página de Razor, un método de acción en un controlador MVC o un software intermedio. El enrutamiento de
ASP.NET Core le permite controlar las direcciones URL usadas por la aplicación.
Para obtener más información, vea Enrutamiento en ASP.NET Core.

Control de errores
ASP.NET Core tiene características integradas para controlar los errores, tales como:
Una página de excepciones para el desarrollador
Páginas de errores personalizados
Páginas de códigos de estado estáticos
Control de excepciones de inicio
Para obtener más información, vea Controlar errores en ASP.NET Core.

Realización de solicitudes HTTP


Una implementación de IHttpClientFactory está disponible para crear instancias de HttpClient . Este servicio:
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.
Admite el registro y encadenamiento de varios controladores de delegación para crear una canalización de
middleware de solicitud saliente. 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.
Se integra con Polly, una conocida biblioteca de terceros para el control de errores transitorios.
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.
Para obtener más información, vea Realización de solicitudes HTTP mediante IHttpClientFactory en ASP.NET
Core.

Raíz del contenido


La raíz del contenido es la ruta de acceso base a cualquier contenido privado que usa la aplicación, como sus
archivos de Razor. De forma predeterminada, la raíz del contenido es la ruta de acceso base para el archivo
ejecutable que hospeda la aplicación. Se puede especificar una ubicación alternativa al crear el host.
Para obtener más información, vea Raíz del contenido.
Para obtener más información, vea Raíz del contenido.

Raíz web
La raíz web (también conocida como webroot) es la ruta de acceso base a los recursos públicos y estáticos, como
archivos de imágenes, CSS y JavaScript. De forma predeterminada, el software intermedio de archivos estáticos
solo ofrecerá archivos desde el directorio raíz web (y subdirectorios). El valor predeterminado de la ruta de acceso
web es {raíz del contenido }/wwwroot, pero se puede especificar una ubicación diferente al crear el host.
En los archivos de Razor ( .cshtml), la virgulilla ~/ apunta a la raíz web. Las rutas de acceso que empiezan por ~/
se conocen como rutas de acceso virtuales.
Para obtener más información, vea Archivos estáticos en ASP.NET Core.
Inicio de la aplicación en ASP.NET Core
20/05/2019 • 12 minutes to read • Edit Online

Tom Dykstra, Luke Latham y Steve Smith


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 :
Incluye opcionalmente un método ConfigureServices para configurar los servicios de la aplicación. Un
servicio es un componente reutilizable que proporciona funcionalidades de la aplicación. Los servicios se
configuran o, como también se denomina, se registran en ConfigureServices y se usan en la aplicación a
través de la inserción de dependencias (DI) o ApplicationServices.
Incluye un método Configure para crear la canalización de procesamiento de solicitudes de la aplicación.
El tiempo de ejecución ASP.NET Core llama a ConfigureServices y Configure cuando la aplicación se inicia:

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

La clase Startup se especifica para la aplicación al generar su host. El host de la aplicación se genera cuando
Build se llama en el generador de host de la clase Program . Habitualmente, la clase Startup se especifica
mediante una llamada al método WebHostBuilderExtensions.UseStartup<TStartup > en el generador de 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>();
}

El host proporciona 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.


Cuando la aplicación define clases Startup independientes para otros entornos (por ejemplo,
StartupDevelopment ), 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 el host, vea El host. 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
El método ConfigureServices es:
Opcional.
Lo llama el host 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.

El host puede configurar algunos servicios antes de que se llame a los métodos Startup . Para obtener más
información, vea El host.
Para las características que requieren una configuración sustancial, hay métodos de extensión Add{Service}
en IServiceCollection. Una aplicación ASP.NET Core típica registra los servicios de Entity Framework, Identity
y MVC:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

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

// Add application services.


services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}

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 a través de la inserción de dependencias o desde ApplicationServices.

Vea SetCompatibilityVersion para obtener más información sobre SetCompatibilityVersion .

El método Configure
El método Configure se usa 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 de ASP.NET Core configuran la canalización con compatibilidad para lo siguiente:
Página de excepciones para el desarrollador
Controlador de excepciones
Seguridad de transporte estricta de HTTP (HSTS )
Redireccionamiento de HTTPS
Archivos estáticos
Reglamento General de Protección de Datos (GDPR )
ASP.NET Core MVC y Razor Pages
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();
}

Cada método de extensión Use agrega uno o más componentes 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.
También se pueden especificar servicios adicionales, como IHostingEnvironment y ILoggerFactory , en la firma
del método Configure . Cuando se especifican, esos servicios adicionales se insertan si están disponibles.
Para obtener más información sobre cómo usar IApplicationBuilder y el orden de procesamiento de
middleware, consulte Middleware de ASP.NET Core.

Métodos de conveniencia
Para configurar los servicios y la canalización de procesamiento de solicitudes sin usar una clase Startup ,
llame a los métodos de conveniencia ConfigureServices y Configure en el generador de host. Varias llamadas
a ConfigureServices se anexan entre sí. Si hay varias llamadas al método Configure , se usa la última llamada
a Configure .
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) =>
{
})
.ConfigureServices(services =>
{
...
})
.Configure(app =>
{
var loggerFactory = app.ApplicationServices
.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<Program>();
var env = app.ApplicationServices.GetRequiredServices<IHostingEnvironment>();
var config = app.ApplicationServices.GetRequiredServices<IConfiguration>();

logger.LogInformation("Logged in Configure");

if (env.IsDevelopment())
{
...
}
else
{
...
}

var configValue = config["subsection:suboption1"];

...
});
}

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.
En el ejemplo siguiente, se muestra cómo registrar un componente de middleware con IStartupFilter .
El componente de middleware RequestSetOptionsMiddleware establece un valor de opciones de un parámetro
de cadena de consulta:

public class RequestSetOptionsMiddleware


{
private readonly RequestDelegate _next;
private IOptions<AppOptions> _injectedOptions;

public RequestSetOptionsMiddleware(
RequestDelegate next, IOptions<AppOptions> injectedOptions)
{
_next = next;
_injectedOptions = injectedOptions;
}

public async Task Invoke(HttpContext httpContext)


{
Console.WriteLine("RequestSetOptionsMiddleware.Invoke");

var option = httpContext.Request.Query["option"];

if (!string.IsNullOrWhiteSpace(option))
{
_injectedOptions.Value.Option = WebUtility.HtmlEncode(option);
}

await _next(httpContext);
}
}

RequestSetOptionsMiddleware está configurado en las clase RequestSetOptionsStartupFilter :

public class RequestSetOptionsStartupFilter : IStartupFilter


{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<RequestSetOptionsMiddleware>();
next(builder);
};
}
}

IStartupFilter está registrado en el contenedor de servicios de ConfigureServices y 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, vea Uso de
ensamblados de inicio de hospedaje en ASP.NET Core.

Recursos adicionales
El host
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
04/07/2019 • 29 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.");
}
}
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);
}

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

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.
El contenedor resuelve ILogger<TCategoryName> aprovechando las ventajas de los
tipos abiertos (genéricos) , lo que elimina la necesidad de registrar todos los tipos
construidos (genéricos):

services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>));

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

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 del servicio requiere un tipo integrado, 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.");
}
}

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.IApplic Transitorio
ationBuilderFactory

Microsoft.AspNetCore.Hosting.IApplicationLif Singleton
etime

Microsoft.AspNetCore.Hosting.IHostingEnvir Singleton
onment

Microsoft.AspNetCore.Hosting.IStartup Singleton

Microsoft.AspNetCore.Hosting.IStartupFilter Transitorio

Microsoft.AspNetCore.Hosting.Server.IServer Singleton

Microsoft.AspNetCore.Http.IHttpContextFact Transitorio
ory

Microsoft.Extensions.Logging.ILogger<T> Singleton

Microsoft.Extensions.Logging.ILoggerFactory Singleton

Microsoft.Extensions.ObjectPool.ObjectPoolP Singleton
rovider

Microsoft.Extensions.Options.IConfigureOpti Transitorio
ons<T>

Microsoft.Extensions.Options.IOptions<T> Singleton

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("DefaultConnection")));

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 el contenedor del
servicio los solicita. 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 del cliente
(conexión).

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 más información, consulte
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 asistentes de etiquetas, controladores
MVC 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


Los contextos de Entity Framework normalmente se agregan al contenedor de
servicios mediante la duración con ámbito porque las operaciones de base de datos
de aplicación web se suelen limitar a la solicitud de cliente. La duración
predeterminada se limita si no se especifica una duración mediante una sobrecarga
de AddDbContext<TContext> al registrar el contexto de base de datos. En los
servicios de una duración determinada no se debe usar un contexto de base de
datos con una duración más corta que el servicio.

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


{
}

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


}

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 al contenedor, el elemento
OperationId del servicio IOperationTransient es diferente del objeto
OperationId de OperationService . OperationService recibe una instancia nueva
de la clase IOperationTransient . La nueva instancia produce un objeto
OperationId diferente.
Si se crean servicios con ámbito por solicitud de cliente, el objeto OperationId
del servicio IOperationScoped es el mismo que para OperationService dentro de
la solicitud de cliente. Entre las solicitudes de cliente, ambos servicios comparten
un valor OperationId diferente.
Si se crean servicios singleton y servicios de instancia singleton una vez y se
usan en todas las solicitudes de cliente y servicios, el objeto OperationId 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; }
}

En Startup.ConfigureServices , cada tipo se agrega al contenedor según su duración


con nombre:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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

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. El valor OperationId transitorio de
la primera y la segunda solicitud de cliente varía tanto para las operaciones
OperationService como entre solicitudes de cliente. Se proporciona una nueva
instancia para cada solicitud de servicio y solicitud de cliente.
Los objetos con ámbito son iguales dentro de una solicitud de cliente, pero
varían entre solicitudes de cliente.
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 más información, consulte 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 produce clases que
son más fáciles de probar.

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.
Evite las llamadas de método estático y con estado.
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.
Cree clases de aplicación pequeñas, bien factorizadas y probadas con facilidad.
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());
}
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
Cree servicios de singleton 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
No se admite la resolución de servicio basada en async/await y Task . C#
no admite los constructores asincrónicos, por lo que el patrón recomendado
es usar métodos asincrónicos después de resolver el servicio de manera
sincrónica.
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 que se necesita mediante la inserción de
dependencias.
Evite el acceso estático a servicios (por ejemplo, escribiendo de forma
estática IApplicationBuilder.ApplicationServices para usarlo en otro lugar).
Evite el uso del patrón del localizador de servicios. Por ejemplo, no invoque a
GetService para obtener una instancia de servicio si puede usar la inserción
de dependencias en su lugar:
Incorrecto:

public void MyMethod()


{
var options =
_services.GetService<IOptionsMonitor<MyOptions>>();
var option = options.CurrentValue.Option;

...
}

Correcto:
private readonly MyOptions _options;

public MyClass(IOptionsMonitor<MyOptions> options)


{
_options = options.CurrentValue;
}

public void MyMethod()


{
var option = _options.Option;

...
}

Otra variación del localizador de servicios que se debe evitar es insertar una
fábrica que resuelva dependencias en tiempo de ejecución. Estas dos
prácticas combinan estrategias de Inversión de control.
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 a los 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
ASP.NET Core Blazor dependency injection
Inicio de la aplicación 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 )
Principio de dependencias explícitas
Los contenedores de inversión de control y el patrón de inserción de
dependencias (Martin Fowler)
Cómo registrar un servicio con varias interfaces de inserción de dependencias
de ASP.NET Core
Middleware de ASP.NET Core
05/07/2019 • 18 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 del 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. Cuando un middleware se
cortocircuita, se llama middleware de terminal porque impide el procesamiento de la solicitud por parte
de middleware adicional.
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 ejemplos
adicionales de middleware.

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. Los delegados que controlan
excepciones deben llamarse al principio de la canalización para que puedan capturar las excepciones
que se producen en las fases siguientes de la canalización.
La aplicación ASP.NET Core más sencilla posible configura un solo delegado de solicitudes que controla
todas las solicitudes. En este caso no se incluye una canalización de solicitudes real. En su lugar, solo se
llama a una única función anónima en respuesta a todas las solicitudes HTTP.

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

Cuando un delegado no pasa una solicitud al siguiente delegado, se denomina cortocircuitar la


canalización de solicitudes. Este proceso es necesario muchas veces, ya que previene la realización de
trabajo innecesario. Por ejemplo, el middleware de archivos estáticos puede actuar como middleware de
terminal procesando una solicitud para un archivo estático y cortocircuitando el resto de la canalización.
El middleware agregado a la canalización antes del middleware que finaliza el procesamiento sigue
procesando código después de sus instrucciones next.Invoke . Sin embargo, consulte la siguiente
advertencia sobre intentar escribir en una respuesta que ya se ha enviado.
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.

Ordenar
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 siguiente método Startup.Configure agrega los componentes de middleware para escenarios de
aplicaciones comunes:
1. Control de errores y excepciones
Cuando la aplicación se ejecuta en el entorno de desarrollo:
El middleware de la página de excepciones para el desarrollador
(UseDeveloperExceptionPage) informa los errores en tiempo de ejecución de la
aplicación.
El middleware de la página de errores de la base de datos (UseDatabaseErrorPage)
informa los errores en tiempo de ejecución de la base de datos.
Cuando la aplicación se ejecuta en el entorno de producción:
El middleware del controlador de excepciones (UseExceptionHandler) detecta las
excepciones generadas en los middlewares siguientes.
El middleware del protocolo de seguridad de transporte estricta de HTTP (HSTS )
(UseHsts) agrega el encabezado Strict-Transport-Security .
2. El middleware de redireccionamiento de HTTPS (UseHttpsRedirection) redirige las solicitudes HTTP
a HTTPS.
3. El middleware de archivos estáticos (UseStaticFiles) devuelve archivos estáticos y genera un
cortocircuito más allá del procesamiento de la solicitud.
4. El middleware de directivas de cookies (UseCookiePolicy) permite que la aplicación cumpla con las
normas del Reglamento general de protección de datos (RGPD ) de la Unión Europea.
5. El middleware de autenticación (UseAuthentication) intenta autenticar al usuarios antes de que se le
permita acceder a los recursos protegidos.
6. El middleware de sesiones (UseSession) establece y mantiene el estado de sesión. Si la aplicación
usa el estado de sesión, llame al middleware de sesiones después del middleware de directivas de
cookies y antes del middleware de MVC.
7. MVC (UseMvc) para agregar MVC a la canalización de la solicitud.
public void Configure(IApplicationBuilder app)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseSession();
app.UseMvc();
}

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 middleware
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.
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 File 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.

REQUEST 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 para 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.

REQUEST 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 middleware en la canalización de procesamiento de
solicitudes, así como las condiciones con las que podría finalizar el procesamiento de solicitudes.
Cuando un middleware cortocircuita la canalización de procesamiento de solicitudes e impide el
procesamiento de una solicitud por parte de middleware descendente adicional, se llama middleware de
terminal. Para más información sobre cómo cortocircuitar, consulte la sección Creación de una
canalización de middleware con IApplicationBuilder.

SOFTWARE INTERMEDIO DESCRIPCIÓN ORDENAR

Autenticación Proporciona compatibilidad con Antes de que se necesite


autenticación. HttpContext.User . Terminal para
devoluciones de llamadas OAuth.

Directiva de cookies Realiza un seguimiento del Antes del middleware que emite las
consentimiento de los usuarios para cookies. Ejemplos: autenticación,
almacenar información personal y sesión y MVC (TempData).
aplica los estándares mínimos para
los campos de las cookies, como
secure y SameSite .

CORS Configura el uso compartido de Antes de los componentes que


recursos entre orígenes. usan CORS.

Control de excepciones Controla las excepciones. 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, IP de
cliente y método.
SOFTWARE INTERMEDIO DESCRIPCIÓN ORDENAR

Comprobación de estado Comprueba el estado de una Terminal si una solicitud coincide


aplicación ASP.NET Core y sus con un punto de conexión de
dependencias, como la comprobación de estado.
comprobación de disponibilidad de
base de datos.

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 Redirija todas las solicitudes HTTP a Antes de los componentes que
HTTPS. consumen la dirección URL.

Seguridad de transporte estricta de Middleware de mejora de seguridad Antes de que se envíen las
HTTP (HSTS) que agrega un encabezado de respuestas y después de los
respuesta especial. componentes que modifican las
solicitudes. Ejemplos: encabezados
reenviados y reescritura de URL.

MVC Procesa las solicitudes con Si hay una solicitud que coincida
MVC/Razor Pages. con una ruta, será final.

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.
Recursos adicionales
Escritura de middleware de ASP.NET Core personalizado
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
Host genérico de .NET
17/05/2019 • 21 minutes to read • Edit Online

Por Luke Latham


Las aplicaciones 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.
En este artículo se describe el host genérico de .NET Core ( HostBuilder).
El host genérico se diferencia del host web en que desacopla la canalización HTTP de la API de host web para
habilitar una mayor variedad de escenarios de host. La mensajería, las tareas en segundo plano y otras cargas
de trabajo que no son de HTTP pueden usar el host genérico y beneficiarse de funcionalidades transversales,
como la configuración, la inserción de dependencias (DI) y el registro.
A partir de ASP.NET Core 3.0, se recomienda el host genérico para las cargas de trabajo de HTTP y las que no
sean de HTTP. Una implementación de servidor HTTP, si se incluye, se ejecuta como una implementación de
IHostedService. IHostedService es una interfaz que también se puede usar para otras cargas de trabajo.
El host web ya no se recomienda para las aplicaciones web, pero sigue estando disponible para la
compatibilidad con versiones anteriores.

NOTE
El resto de este artículo no se ha actualizado para la versión 3.0.

Las aplicaciones 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.
En este artículo se trata el host genérico de ASP.NET Core ( HostBuilder), que se usa para hospedar
aplicaciones que no procesan solicitudes HTTP.
El objetivo del host genérico es desacoplar la canalización HTTP de la API host web para habilitar una mayor
variedad de escenarios de host. La mensajería, las tareas en segundo plano y otras cargas de trabajo que no
son de HTTP basadas en la ventaja de host genérico se benefician de funcionalidades 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 reemplazará al host web en una próxima
versión y actuará como la API de host principal tanto en escenarios de HTTP como los que no lo son.
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 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();
}

Opciones
HostOptions configura opciones para IHost.
Tiempo de espera de apagado
ShutdownTimeout establece el tiempo de espera para StopAsync. El valor predeterminado es cinco segundos.
La siguiente configuración de opción en Program.Main aumenta el tiempo de espera de apagado
predeterminado de 5 segundos a 20 segundos:

var host = new HostBuilder()


.ConfigureServices((hostContext, services) =>
{
services.Configure<HostOptions>(option =>
{
option.ShutdownTimeout = System.TimeSpan.FromSeconds(20);
});
})
.Build();

Servicios predeterminados
Estos son los servicios que se registran durante la inicialización del host:
Entorno (IHostingEnvironment)
HostBuilderContext
Configuración (IConfiguration)
IApplicationLifetime (ApplicationLifetime)
IHostLifetime (ConsoleLifetime)
IHost
Opciones (AddOptions)
Registro (AddLogging)
Configuración de host
La configuración del host se crea:
Llamando a métodos de extensión en IHostBuilder para establecer la raíz del contenido y el entorno.
Leyendo la configuración desde los proveedores de configuración de ConfigureHostConfiguration.
Métodos de extensión
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_>APPLICATIONNAME ( <PREFIX_> es opcional y definido por el usuario)
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)

ConfigureHostConfiguration
ConfigureHostConfiguration usa un IConfigurationBuilder para crear una IConfiguration para el host. La
configuración del host se usa para inicializar el IHostingEnvironment para su uso en el proceso de
compilación de la aplicación.
Se puede llamar varias veces a ConfigureHostConfiguration con resultados de suma. El host usa cualquier
opción que establezca un valor en último lugar en una clave determinada.
La configuración del host fluye automáticamente a la configuración de la aplicación
(ConfigureAppConfiguration y al resto de la aplicación).
De forma predeterminada no se incluye ningún proveedor. Debe especificar de forma explícita los
proveedores de configuración que requiere la aplicación en ConfigureHostConfiguration, así como:
La configuración de archivo (por ejemplo, de un archivo hostsettings.json).
La configuración de la variable de entorno.
La configuración del argumento de la línea de comandos.
Otros proveedores de configuración necesarios.
La configuración de archivo del host se habilita especificando la ruta base de la aplicación con SetBasePath
seguido de una llamada a uno de los proveedores de configuración de archivo. La aplicación de ejemplo usa
un archivo JSON, hostsettings.json, y llama a AddJsonFile para consumir los valores de configuración del host
del archivo.
Para agregar una configuración de la variable de entorno del host, llame a AddEnvironmentVariables en el
generador del host. 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.
La configuración de la línea de comandos se agrega llamando a AddCommandLine. La configuración de la
línea de comandos se agrega en último lugar para permitir que los argumentos de la línea de comandos
invaliden la configuración proporcionada por los proveedores de configuración anteriores.
hostsettings.json:

{
"environment": "Development"
}

Se puede proporcionar una configuración adicional con las claves applicationName y contentRoot.
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);
})

ConfigureAppConfiguration
La configuración de la aplicación se crea llamando a ConfigureAppConfiguration en la implementación de
IHostBuilder. ConfigureAppConfiguration usa un IConfigurationBuilder para crear una IConfiguration para la
aplicación. Se puede llamar varias veces a ConfigureAppConfiguration con resultados de suma. La aplicación
usa cualquier opción que establezca un valor en último lugar en una clave determinada. La configuración
creada por ConfigureAppConfiguration está disponible en HostBuilderContext.Configuration para las
operaciones posteriores y en Services.
La configuración de la aplicación recibe automáticamente la configuración del host proporcionada por
ConfigureHostConfiguration.
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"
}
}
}

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" Exclude="bin\**\*;obj\**\*"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

NOTE
Los métodos de extensión de configuración, como AddJsonFile y AddEnvironmentVariables requieren paquetes NuGet
adicionales, como Microsoft.Extensions.Configuration.Json y Microsoft.Extensions.Configuration.EnvironmentVariables. A
menos que la aplicación use el metapaquete Microsoft.AspNetCore.App, además del paquete principal
Microsoft.Extensions.Configuration, se deben agregar estos paquetes. Para obtener más información, vea Configuración
en ASP.NET Core.

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) =>
{
if (hostContext.HostingEnvironment.IsDevelopment())
{
// Development service configuration
}
else
{
// Non-development service configuration
}

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<TContainerBuilder>. 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 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));
}
}
}

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
Host web de ASP.NET Core
02/07/2019 • 30 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. El host también puede configurar el registro, la inserción de dependencias y la
configuración.
En el artículo se explica el host web, que solo permanece disponible para compatibilidad con versiones
anteriores: Se recomienda el host genérico para todos los tipos de aplicaciones.
En este artículo se describe el host web, que sirve para hospedar aplicaciones web. Para otros tipos de
aplicaciones, use el host genérico.

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. Una aplicación típica llama 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>();
}

El código que llama a CreateDefaultBuilder está en un método denominado CreateWebHostBuilder , que lo


diferencia del código de Main que llama a Run en el objeto generador. Dicha diferencia es necesaria si usa
herramientas de Entity Framework Core. Las herramientas esperan encontrar un método CreateWebHostBuilder
al que pueden llamar en tiempo de diseño para configurar el host sin ejecutar la aplicación. Una alternativa
consiste en implementar IDesignTimeDbContextFactory . Para obtener más información, consulte Creación de
DbContext en tiempo de diseño.
CreateDefaultBuilder realiza las tareas siguientes:
Configura el servidor Kestrel como servidor web por medio de los proveedores de configuración de
hospedaje de la aplicación. Para conocer las opciones predeterminadas del servidor Kestrel, consulte
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 la aplicación en el siguiente orden:
appsettings.json.
appsettings.{Environment}.json.
Administrador de secretos 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 con el módulo ASP.NET Core, CreateDefaultBuilder habilita la integración
de IIS, que configura la dirección base y el puerto de la aplicación. La integración de IIS 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.

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 más información, consulte 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_APPLICATIONNAME

WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.ApplicationKey, "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)

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

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

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 más información, consulte Usar varios entornos en
ASP.NET Core.

WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)

Ensamblados de inicio de hospedaje


Establece los ensamblados de inicio de hospedaje de la aplicación.
Clave: hostingStartupAssemblies
Tipo: cadena
Valor predeterminado: 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: cadena Valor predeterminado: no se ha establecido ningún 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


Una cadena delimitada por punto y coma de ensamblados de inicio de hospedaje para excluir en el inicio.
Clave: hostingStartupExcludeAssemblies
Tipo: cadena
Valor predeterminado: Cadena vacía
Establecer mediante: UseSetting
Variable de entorno: ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES
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 más información, consulte Uso de ensamblados de inicio de hospedaje en
ASP.NET Core.
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 de un
servidor a otro.

WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")

Kestrel tiene su propia API de configuración de punto de conexión. Para más información, consulte
Implementación del servidor web Kestrel en ASP.NET Core.
Tiempo de espera de apagado
Especifica la cantidad de tiempo que se espera hasta el cierre del host 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>()

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

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

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

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

REQUEST RESPUESTA

http://localhost:5000/hello/Martin Hello, Martin!

http://localhost:5000/buenosdias/Catrina Buenos dias, Catrina!

http://localhost:5000/throw/ooops! Produce una excepción con la cadena "ooops!"

http://localhost:5000/throw Produce una excepción con la cadena "Uh oh!"

http://localhost:5000/Sante/Kevin Sante, Kevin!

http://localhost:5000 Hello World!

WaitForShutdown se bloquea hasta que se emite un salto (Ctrl-C/SIGINT o SIGTERM ). La aplicación muestra el
mensaje Console.WriteLine y espera a que se pulse una tecla para salir.
Start(string url, Action<IRouteBuilder> routeBuilder)
Use una dirección URL y una instancia de IRouteBuilder :

using (var host = WebHost.Start("http://localhost:8080", router => router


.MapGet("hello/{name}", (req, res, data) =>
res.WriteAsync($"Hello, {data.Values["name"]}!"))
.MapGet("buenosdias/{name}", (req, res, data) =>
res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
.MapGet("throw/{message?}", (req, res, data) =>
throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
.MapGet("{greeting}/{name}", (req, res, data) =>
res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!"))
.MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produce el mismo resultado que Start(Action<IRouteBuilder> routeBuilder) , salvo que la aplicación


responde en http://localhost:8080 .
StartWith(Action<IApplicationBuilder> app)
Proporciona un delegado para configurar IApplicationBuilder :

using (var host = WebHost.StartWith(app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Haga una solicitud en el explorador a http://localhost:5000 para recibir la respuesta "Hello World!"
WaitForShutdown se bloquea hasta que se emite un salto ( Ctrl-C/SIGINT o SIGTERM ). La aplicación muestra el
mensaje Console.WriteLine y espera a que se pulse una tecla para salir.
StartWith(string url, Action<IApplicationBuilder> app)
Proporcione una dirección URL y un delegado para configurar IApplicationBuilder :

using (var host = WebHost.StartWith("http://localhost:8080", app =>


app.Use(next =>
{
return async context =>
{
await context.Response.WriteAsync("Hello World!");
};
})))
{
Console.WriteLine("Use Ctrl-C to shut down the host...");
host.WaitForShutdown();
}

Produce el mismo resultado que StartWith(Action<IApplicationBuilder> app) , salvo que la aplicación


responde en http://localhost:8080 .

Interfaz IHostingEnvironment
La interfaz IHostingEnvironment proporciona información sobre el entorno de hospedaje web de la aplicación.
Use inserción de constructores para obtener IHostingEnvironment a fin de utilizar sus propiedades y métodos de
extensión:

public class CustomFileReader


{
private readonly IHostingEnvironment _env;

public CustomFileReader(IHostingEnvironment env)


{
_env = env;
}

public string ReadFile(string filePath)


{
var fileProvider = _env.WebRootFileProvider;
// Process the file here
}
}
Puede utilizarse un enfoque convencional para configurar la aplicación en el inicio según el entorno. Como
alternativa, inserte IHostingEnvironment en el constructor Startup para su uso en ConfigureServices :

public class Startup


{
public Startup(IHostingEnvironment env)
{
HostingEnvironment = env;
}

public IHostingEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)


{
if (HostingEnvironment.IsDevelopment())
{
// Development configuration
}
else
{
// Staging/Production configuration
}

var contentRootPath = HostingEnvironment.ContentRootPath;


}
}

NOTE
Además del método de extensión IsDevelopment , IHostingEnvironment ofrece los métodos IsStaging ,
IsProduction y IsEnvironment(string environmentName) . Para más información, consulte 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;
})

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
Implementaciones de servidores web en ASP.NET
Core
17/06/2019 • 15 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 un
conjunto de características de solicitud compuestos en un HttpContext.

Kestrel
Kestrel es el servidor web predeterminado que se incluye en las plantillas de proyecto de ASP.NET Core.
Use Kestrel:
Por sí solo como un servidor perimetral que procesa solicitudes directamente desde una red, incluida
Internet.

Con un servidor proxy inverso, tal como Internet Information Services (IIS ), Nginx o Apache. Un servidor
proxy inverso recibe las solicitudes HTTP de Internet y las reenvía a Kestrel.

Se admite cualquiera de las configuraciones de hospedaje, — con o sin un servidor proxy inverso— para
ASP.NET Core 2.1 o aplicaciones posteriores.
Si quiere obtener instrucciones e información sobre la configuración de Kestrel y su uso en una configuración de
proxy inverso, vea Implementación del servidor web Kestrel en ASP.NET Core.
Windows
macOS
Linux
ASP.NET Core se suministra con los siguientes componentes:
El servidor Kestrel es la implementación de servidor HTTP multiplataforma predeterminada.
El servidor HTTP de IIS es un servidor en proceso de IIS.
El servidor HTTP.sys es un servidor HTTP solo de Windows basado en el controlador del kernel HTTP.sys y la
API de servidor HTTP.
Cuando se usa IIS o IIS Express, la aplicación se ejecuta:
En el mismo proceso que el proceso de trabajo de IIS (el modelo de hospedaje dentro de proceso) con el
servidor HTTP de IIS. La configuración recomendada es En proceso.
En un proceso distinto al del proceso de trabajo de IIS (el modelo de hospedaje fuera de proceso) con el
servidor Kestrel.
El módulo ASP.NET Core es un módulo nativo de IIS que controla las solicitudes de IIS nativas entre IIS y el
servidor de IIS en proceso o Kestrel. Para obtener más información, vea Módulo ASP.NET Core.

Modelos de hospedaje
Con el hospedaje en proceso, una aplicación ASP.NET Core se ejecuta en el mismo proceso que su proceso de
trabajo de IIS. El hospedaje en proceso proporciona un rendimiento mejorado con respecto al hospedaje fuera de
proceso porque las solicitudes no se realizan mediante proxy en el adaptador de bucle invertido, una interfaz de
red que devuelve el tráfico saliente a la misma máquina. IIS controla la administración de procesos con el
Servicio de activación de procesos de Windows (WAS ).
Con el hospedaje fuera de proceso, las aplicaciones ASP.NET Core se ejecutan en un proceso independiente del
proceso de trabajo de IIS, y el módulo 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 apaga o se
bloquea. Este comportamiento es básicamente el mismo que el de las aplicaciones que se ejecutan en proceso y
se administran a través del Servicio de activación de procesos de Windows (WAS ).
Para obtener más información e instrucciones de configuración, vea los temas siguientes:
Hospedaje de ASP.NET Core en Windows con IIS
Módulo ASP.NET Core
Windows
macOS
Linux
ASP.NET Core se suministra con los siguientes componentes:
El servidor Kestrel es el servidor HTTP multiplataforma predeterminado.
El servidor HTTP.sys es un servidor HTTP solo de Windows basado en el controlador del kernel HTTP.sys y la
API de servidor HTTP.
Al usar IIS o IIS Express, la aplicación se ejecuta en un proceso distinto al del proceso de trabajo de IIS (fuera de
proceso) con el servidor Kestrel.
Dado que las aplicaciones ASP.NET Core se ejecutan en un proceso independiente del proceso de trabajo de IIS,
el módulo 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 apaga o se bloquea. Este comportamiento
es básicamente el mismo que el de las aplicaciones que se ejecutan en proceso 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 IIS, el módulo ASP.NET Core y una aplicación hospedada
fuera de proceso:

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 la 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. El middleware agregado por la integración de IIS actualiza el esquema, la dirección IP remota y
PathBase para responder del reenvío de la solicitud a Kestrel. 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.
Para obtener instrucciones de configuración para IIS y el módulo ASP.NET Core, consulte los temas siguientes:
Hospedaje de ASP.NET Core en Windows con IIS
Módulo ASP.NET Core
Nginx con Kestrel
Para información sobre cómo usar Nginx en Linux como servidor proxy inverso para Kestrel, consulte Hospedar
ASP.NET Core en Linux con Nginx.
Apache con Kestrel
Para información sobre cómo usar Apache en Linux como servidor proxy inverso para Kestrel, consulte Hospedar
ASP.NET Core en Linux con Apache.

HTTP.sys
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 funcionalidades necesarias son compatibles con HTTP.sys pero no con Kestrel. Para
obtener más información, vea Implementación del servidor web HTTP.sys en ASP.NET Core.

HTTP.sys también se puede usar para las aplicaciones que solo se exponen a una red interna.

Para obtener instrucciones de configuración de HTTP.sys, vea Implementación del servidor web HTTP.sys en
ASP.NET Core.

Infraestructura de servidores de ASP.NET Core


El elemento IApplicationBuilder disponible en el método Startup.Configure expone la propiedad ServerFeatures
del tipo IFeatureCollection. Kestrel y HTTP.sys 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 las interfaces de la característica que usa la aplicación
requieren implementación, aunque como mínimo se debe admitir IHttpRequestFeature y IHttpResponseFeature.

Inicio del servidor


El servidor se inicia cuando el entorno de desarrollo integrado (IDE ) o editor inicia la aplicación:
Visual Studio: los perfiles de inicio se pueden usar para iniciar la aplicación y el servidor con IIS
Express/módulo de ASP.NET Core o la consola.
Visual Studio Code: la aplicación y el servidor se inician mediante Omnisharp, con lo que se activa el
depurador CoreCLR.
Visual Studio para Mac: la aplicación y el servidor se inician mediante Mono Soft-Mode Debugger.
Al iniciar la 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 dotnet run y Empaquetado de distribución de .NET Core.

Compatibilidad con HTTP/2


HTTP/2 es compatible con ASP.NET Core en los escenarios de implementación siguientes:
Kestrel
Sistema operativo
Windows Server 2016/Windows 10 o posterior†
Linux con OpenSSL 1.0.2 o posterior (por ejemplo, Ubuntu 16.04 o posterior)
HTTP/2 se admitirá en una versión futura en macOS.
Plataforma de destino: .NET Core 2.2 o posterior
HTTP.sys
Windows Server 2016/Windows 10 o posterior
Marco de destino: No aplicable a implementaciones de HTTP.sys.
IIS (en proceso)
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Plataforma de destino: .NET Core 2.2 o posterior
IIS (fuera de proceso)
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Las conexiones de servidor perimetral de acceso público usan HTTP/2, pero la conexión de proxy
inverso a Kestrel usa HTTP/1.1.
Marco de destino: No aplicable a implementaciones fuera de proceso de IIS.
†Kestrel tiene compatibilidad limitada para HTTP/2 en Windows Server 2012 R2 y Windows 8.1. La
compatibilidad es limitada porque la lista de conjuntos de cifrado TLS admitidos y disponibles en estos sistemas
operativos está limitada. Se puede requerir un certificado generado mediante Elliptic Curve Digital Signature
Algorithm (ECDSA) para proteger las conexiones TLS.
HTTP.sys
Windows Server 2016/Windows 10 o posterior
Marco de destino: No aplicable a implementaciones de HTTP.sys.
IIS (fuera de proceso)
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Las conexiones de servidor perimetral de acceso público usan HTTP/2, pero la conexión de proxy
inverso a Kestrel usa HTTP/1.1.
Marco de destino: No aplicable a implementaciones fuera de proceso de IIS.
Una conexión HTTP/2 debe usar Application-Layer Protocol Negotiation (ALPN ) y TLS 1.2 o posterior. Para más
información, vea los temas que pertenecen a los escenarios de implementación de servidor.

Recursos adicionales
Implementación del servidor web Kestrel en ASP.NET Core
Módulo ASP.NET Core
Hospedaje de ASP.NET Core en Windows con IIS
Implementar aplicaciones de ASP.NET Core en Azure App Service
Hospedar ASP.NET Core en Linux con Nginx
Hospedar ASP.NET Core en Linux con Apache
Implementación del servidor web HTTP.sys en ASP.NET Core
Configuración en ASP.NET Core
12/06/2019 • 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
Los paquetes de configuración para escenarios comunes de proveedores de configuración se
incluyen en el metapaquete Microsoft.AspNetCore.App. Los ejemplos de código que siguen y en
la aplicación de ejemplo utilizan el espacio de nombres Microsoft.Extensions.Configuration:

using Microsoft.Extensions.Configuration;

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)

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 obtener 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 El host.

Configuración predeterminada
Las aplicaciones web basadas en las plantillas de dotnet new de ASP.NET Core llaman a
CreateDefaultBuilder al crear un host. CreateDefaultBuilder proporciona la configuración
predeterminada para la aplicación en el orden siguiente:
La configuración del host la proporcionan los siguientes elementos:
Variables de entorno prefijadas con ASPNETCORE_ (por ejemplo,
ASPNETCORE_ENVIRONMENT ) con el Proveedor de configuración de variables de entorno. El
prefijo ( ASPNETCORE_ ) se quita cuando se cargan los pares clave-valor de configuración.
Argumentos de la línea de comandos con el Proveedor de configuración de línea de
comandos.
La configuración de la aplicación la proporcionan los siguientes elementos:
appsettings.json con el Proveedor de configuración de archivo.
appsettings.{Entorno }.json con el Proveedor de configuración de archivo.
Administrador de secretos cuando la aplicación se ejecuta en el entorno Development
por medio del ensamblado de entrada.
Variables de entorno con el Proveedor de configuración de variables de entorno. Si se
usa un prefijo personalizado (por ejemplo, PREFIX_ con
.AddEnvironmentVariables(prefix: "PREFIX_") ), el prefijo se quita cuando se cargan los
pares clave y valor de configuración.
Argumentos de la línea de comandos con el Proveedor de configuración de línea de
comandos.
Los proveedores de configuración se explican posteriormente en este tema. Para obtener más
información sobre el host y CreateDefaultBuilder, vea Host web de 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 almacén de claves de Azure 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. GetSection está en el
paquete Microsoft.Extensions.Configuration, que está en el metapaquete
Microsoft.AspNetCore.App.

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 que implementan la detección de cambios tienen la capacidad
de volver a cargar la configuración cuando se cambia una configuración subyacente. Por
ejemplo, el proveedor de configuración de archivos (descrito más adelante en este tema) y el
proveedor de configuración de Azure Key Vault implementan la detección de cambios.
IConfiguration está disponible en el contenedor de inserción de dependencias (DI) de la
aplicación. IConfiguration se puede insertar en PageModel de Razor Pages para obtener la
configuración de la clase:

public class IndexModel : PageModel


{
private readonly IConfiguration _config;

public IndexModel(IConfiguration config)


{
_config = config;
}

// The _config local variable is used to obtain configuration


// throughout the class.
}

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 Azure Key Vault


(temas 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 clave por archivo Archivos de directorio

Proveedor de configuración de memoria Colecciones en memoria

Secretos de usuario (Administrador de secretos) Archivo en el directorio del perfil de usuario


(temas 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. Azure Key Vault
3. Secretos de usuario (Administrador de secretos) (solo en el entorno de desarrollo)
4. Variables de entorno
5. 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.

ConfigureAppConfiguration
Llame a ConfigureAppConfiguration cuando cree el host para especificar los proveedores de
configuración de la aplicación además de aquellos que CreateDefaultBuilder agrega
automáticamente:

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

La configuración proporcionada a la aplicación en ConfigureAppConfiguration está disponible


durante el inicio de la aplicación, incluido Startup.ConfigureServices . Para obtener más
información, vea la sección Acceso a la configuración durante el inicio.
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, se llama 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.
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de la
aplicación.
Ya se ha llamado a AddCommandLine por parte de CreateDefaultBuilder . Si tiene que
proporcionar la configuración de la aplicación y mantener la posibilidad de invalidar dicha
configuración con argumentos de línea de comandos, llame a los proveedores adicionales de la
aplicación en ConfigureAppConfiguration y llame por último a AddCommandLine .

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
// Call other providers here and call AddCommandLine last.
config.AddCommandLine(args);
})
.UseStartup<Startup>();
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:


var config = new ConfigurationBuilder()
// Call additional providers here as needed.
// Call AddCommandLine last to allow arguments to override other configuration.
.AddCommandLine(args)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo aprovecha el método de conveniencia estático CreateDefaultBuilder
para crear el host, que incluye una llamada a AddCommandLine.
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=value1 --CommandLineKey2=value2 /CommandLineKey3=value3


dotnet run --CommandLineKey1 value1 /CommandLineKey2 value2
dotnet run CommandLineKey1= CommandLineKey2=value2

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.
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de la
aplicación:

public class Program


{
public static readonly Dictionary<string, string> _switchMappings =
new Dictionary<string, string>
{
{ "-CLKey1", "CommandLineKey1" },
{ "-CLKey2", "CommandLineKey2" }
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

// Do not pass the args to CreateDefaultBuilder


public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddCommandLine(args, _switchMappings);
})
.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.
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 (por ejemplo, Bash). 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 Aplicaciones de Azure: Invalidación de la
configuración de la aplicación mediante Azure Portal.
AddEnvironmentVariables se usa para cargar variables de entorno con el prefijo ASPNETCORE_
para la configuración de host al inicializar un nuevo elemento WebHostBuilder. Para más
información, consulte Host web: Configuración de un host.
CreateDefaultBuilder también carga:
Configuración de la aplicación desde variables de entorno sin prefijo mediante la llamada a
AddEnvironmentVariables sin prefijo.
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.
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de la
aplicación.
Si tiene que proporcionar la configuración de la aplicación desde variables de entorno
adicionales, llame a los proveedores adicionales de la aplicación en ConfigureAppConfiguration
y llame a AddEnvironmentVariables con el prefijo.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
// Call additional providers here as needed.
// Call AddEnvironmentVariables last if you need to allow
// environment variables to override values from other
// providers.
config.AddEnvironmentVariables(prefix: "PREFIX_");
})
.UseStartup<Startup>();
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var config = new ConfigurationBuilder()


.AddEnvironmentVariables()
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Ejemplo
La aplicación de ejemplo aprovecha el método de conveniencia estático CreateDefaultBuilder
para crear el host, que incluye una llamada a AddEnvironmentVariables .
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:
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.
CLAVE DE CONFIGURACIÓN ENTRADA DE CONFIGURACIÓN DEL
CLAVE DE VARIABLE DE ENTORNO 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.
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de 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)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddIniFile(
"config.ini", optional: true, reloadOnChange: true);
})
.UseStartup<Startup>();
}

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
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.
Llame a ConfigureAppConfiguration al crear el host para especificar la configuración de la
aplicación para archivos distintos de appsettings.json y appsettings.{Environment}.json:

public class Program


{
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.AddJsonFile(
"config.json", optional: true, reloadOnChange: true);
})
.UseStartup<Startup>();
}

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("config.json", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
Ejemplo
La aplicación de ejemplo aprovecha el método de conveniencia estático CreateDefaultBuilder
para crear el host, que incluye una llamada a AddJsonFile . 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.
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de 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)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddXmlFile(
"config.xml", optional: true, reloadOnChange: true);
})
.UseStartup<Startup>();
}

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

var config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.Build();

var host = new WebHostBuilder()


.UseConfiguration(config)
.UseKestrel()
.UseStartup<Startup>();

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
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.
El carácter de subrayado doble ( __ ) se usa como delimitador de claves de configuración en los
nombres de archivo. Por ejemplo, el nombre de archivo Logging__LogLevel__System genera la
clave de configuración Logging:LogLevel:System .
Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de 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)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
var path = Path.Combine(
Directory.GetCurrentDirectory(), "path/to/files");
config.AddKeyPerFile(directoryPath: path, optional: true);
})
.UseStartup<Startup>();
}

Se establece la ruta de acceso base con SetBasePath. SetBasePath está en el paquete


Microsoft.Extensions.Configuration.FileExtensions, que está en el metapaquete
Microsoft.AspNetCore.App.
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>> .

Llame a ConfigureAppConfiguration cuando cree el host para especificar la configuración de la


aplicación:
public class Program
{
public static readonly Dictionary<string, string> _dict =
new Dictionary<string, string>
{
{"MemoryCollectionKey1", "value1"},
{"MemoryCollectionKey2", "value2"}
};

public static void Main(string[] args)


{
CreateWebHostBuilder(args).Build().Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddInMemoryCollection(_dict);
})
.UseStartup<Startup>();
}

Al crear directamente un WebHostBuilder, llame a UseConfiguration con la configuración:

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.
En el ejemplo siguiente:
Se extrae el valor de cadena de la configuración con la clave NumberKey . Si NumberKey no se
encuentra en las claves de configuración, se usa el valor predeterminado de 99 .
Se escribe el valor como int .
Se almacena el valor en la propiedad NumberConfig para usarlo en la página.
public class IndexModel : PageModel
{
public IndexModel(IConfiguration config)
{
_config = config;
}

public int NumberConfig { get; private set; }

public void OnGet()


{
NumberConfig = _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. GetSection está en el paquete Microsoft.Extensions.Configuration, que está en el
metapaquete Microsoft.AspNetCore.App.
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");

configSection no tiene ningún valor, solo una clave y una ruta de acceso.
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.
Cuando GetSection devuelve una sección coincidente, Value no se rellena. Se devuelven Key y
Path si la sección existe.
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. Bind está en el paquete
Microsoft.Extensions.Configuration.Binder, que está en el metapaquete
Microsoft.AspNetCore.App.
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; }
}

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

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;

GetSection está en el paquete Microsoft.Extensions.Configuration, que está en el metapaquete


Microsoft.AspNetCore.App.

Enlazar a un gráfico de objetos


Bind es capaz de enlazar todo un gráfico de objetos POCO. Bind está en el paquete
Microsoft.Extensions.Configuration.Binder, que está en el metapaquete
Microsoft.AspNetCore.App.
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; }
}

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>

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;

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

Get está en el paquete Microsoft.Extensions.Configuration.Binder, que está en el metapaquete


Microsoft.AspNetCore.App. Get<T> está disponible en ASP.NET Core 1.1 o versiones
posteriores. GetSection está en el paquete Microsoft.Extensions.Configuration, que está en el
metapaquete Microsoft.AspNetCore.App.

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. "Bind" está en el paquete
Microsoft.Extensions.Configuration.Binder, que está en el metapaquete
Microsoft.AspNetCore.App.

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:entries:0 value0

array:entries:1 value1

array:entries:2 value2

array:entries:4 value4

array:entries: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>();
}

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

Los datos de configuración están enlazados al objeto:

var arrayExample = new ArrayExample();


_config.GetSection("array").Bind(arrayExample);

GetSection está en el paquete Microsoft.Extensions.Configuration, que está en el metapaquete


Microsoft.AspNetCore.App.
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>();

El objeto enlazado, una instancia de ArrayExample , recibe los datos de la matriz desde la
configuración.

ÍNDICE DE ARRAYEXAMPLE.ENTRIES VALOR DE ARRAYEXAMPLE.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 ArrayExample 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, ArrayExample.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);

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 ArrayExample se enlaza después de que el proveedor de configuración


JSON incluye la entrada para el índice #3, la matriz ArrayExample.Entries incluye el valor.

ÍNDICE DE ARRAYEXAMPLE.ENTRIES VALOR DE ARRAYEXAMPLE.ENTRIES

0 value0
ÍNDICE DE ARRAYEXAMPLE.ENTRIES VALOR DE ARRAYEXAMPLE.ENTRIES

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"
]
}
}

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

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

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


}

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

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

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

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

Acceso a la configuración durante el inicio


Inserte IConfiguration en el constructor Startup para acceder a los valores de configuración
en 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 ver un ejemplo de cómo acceder a la configuración mediante 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 Uso de ensamblados de inicio de hospedaje en ASP.NET Core.

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
10/05/2019 • 22 minutes to read • Edit Online

Por Luke Latham


Para obtener la versión 1.1 de este tema, descargue Patrón de opciones en ASP.NET Core (versión 1.1, PDF ).
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 ) o encapsulación – 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.
Las opciones también proporcionan un mecanismo para validar los datos de configuración. Para obtener más
información, consulte la sección Opciones de validación.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Haga referencia al metapaquete Microsoft.AspNetCore.App o agregue una referencia de paquete al paquete
Microsoft.Extensions.Options.ConfigurationExtensions.

Interfaces de opciones
IOptionsMonitor<TOptions> se usa para recuperar las opciones y administrar las notificaciones de las
opciones para instancias de TOptions . IOptionsMonitor<TOptions> admite las siguientes situaciones:
Notificaciones de cambios
Opciones con nombre
Configuración que se puede recargar
Invalidación de opciones de selección (IOptionsMonitorCache<TOptions>)
Los escenarios posteriores a la configuración le permiten establecer o cambiar las opciones después de que
finalice toda la configuración de IConfigureOptions<TOptions>.
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<TOptions> y IPostConfigureOptions<TOptions>, y establece todas las configuraciones
primero, seguidas de las configuraciones posteriores. Distingue entre IConfigureNamedOptions<TOptions> y
IConfigureOptions<TOptions>, y solo llama a la interfaz adecuada.
IOptionsMonitorCache<TOptions> se usa por IOptionsMonitor<TOptions> para almacenar en caché las
instancias de TOptions . IOptionsMonitorCache<TOptions> 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.
IOptionsSnapshot<TOptions> es útil en escenarios donde se deben volver a calcular las opciones en cada
solicitud. Para obtener más información, consulte la sección Volver a cargar los datos de configuración con
IOptionsSnapshot.
IOptions<TOptions> puede utilizarse para admitir las opciones. Sin embargo, IOptions<TOptions> no es
compatible con los escenarios anteriores de IOptionsMonitor<TOptions>. Aún puede usar
IOptions<TOptions> en marcos y bibliotecas existentes que ya usan la interfaz IOptions<TOptions> y no
requieren los escenarios proporcionados por IOptionsMonitor<TOptions>.

Configuración de opciones generales


La configuración de opciones generales 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 y se enlaza a la configuración:

// Example #1: General configuration


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


IOptionsMonitor<TOptions> para acceder a la configuración (Pages/Index.cshtml.cs):

private readonly MyOptions _options;

public IndexModel(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_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(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_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 y se aplican en el orden en que están registrados. Para obtener más información, vea
Configuración en ASP.NET Core.
Cada llamada a Configure 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_delegate, 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: Suboptions


// 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(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}

// Example #3: Suboptions


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 IOptionsMonitor<TOptions>
directamente en una vista (Pages/Index.cshtml.cs):

private readonly MyOptions _options;


public IndexModel(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_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;

La aplicación de ejemplo muestra cómo insertar IOptionsMonitor<MyOptions> con una directiva @inject :

@page
@model IndexModel
@using Microsoft.Extensions.Options
@inject IOptionsMonitor<MyOptions> OptionsAccessor
@{
ViewData["Title"] = "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<TOptions> se muestra
en el ejemplo #5 en la aplicación de ejemplo.
IOptionsSnapshot<TOptions> 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.
En el ejemplo siguiente se muestra cómo se crea un nuevo IOptionsSnapshot<TOptions> 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(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_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<TOptions> 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, que 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 Get (Pages/Index.cshtml.cs):

private readonly MyOptions _named_options_1;


private readonly MyOptions _named_options_2;

public IndexModel(
IOptionsMonitor<MyOptions> optionsAccessor,
IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptionsMonitor<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.CurrentValue;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
_subOptions = subOptionsAccessor.CurrentValue;
_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 .

Configurar todas las opciones con el método ConfigureAll


Configure todas las instancias de opciones con el método ConfigureAll. El siguiente código configura Option1
para todas las instancias de configuración con un valor común. Agregue manualmente este código al método
Startup.ConfigureServices :

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 IConfigureOptions<TOptions> existentes se usan para
seleccionar como destino la instancia de Options.DefaultName , que es string.Empty .
IConfigureNamedOptions<TOptions> también implementa IConfigureOptions<TOptions>. La implementación
predeterminada de IOptionsFactory<TOptions> 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).

API OptionsBuilder
OptionsBuilder<TOptions> se usa para configurar instancias TOptions . OptionsBuilder simplifica la creación
de opciones con nombre, ya que es un único parámetro para la llamada
AddOptions<TOptions>(string optionsName) inicial en lugar de aparecer en todas las llamadas posteriores. La
validación de opciones y las sobrecargas ConfigureOptions que aceptan las dependencias de servicio solo
están disponibles mediante OptionsBuilder .

// Options.DefaultName = "" is used.


services.AddOptions<MyOptions>().Configure(o => o.Property = "default");

services.AddOptions<MyOptions>("optionalName")
.Configure(o => o.Property = "named");

Uso de servicios de DI para configurar opciones


Hay dos formas de acceder a otros servicios desde la inserción de dependencias durante la configuración de
opciones:
Si se pasa un delegado de configuración a Configure en OptionsBuilder<TOptions>.
OptionsBuilder<TOptions> proporciona sobrecargas de Configure que permiten usar hasta cinco
servicios para configurar las opciones:

services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));

Si se crea un tipo propio que implementa IConfigureOptions<TOptions> o


IConfigureNamedOptions<TOptions>, y se registrar como un servicio.
Se recomienda pasar un delegado de configuración a Configure, ya que la creación de un servicio es más
complicada. La creación de un tipo propio es equivalente a lo que el marco hace de forma automática cuando
se usa Configure. La llamada a Configure registra una interfaz IConfigureNamedOptions<TOptions>
genérica y transitoria, con un constructor que acepta los tipos de servicio genéricos especificados.

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(IOptionsMonitor<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);
}

La validación basada en la anotación de datos está disponible en el paquete


Microsoft.Extensions.Options.DataAnnotations llamando al método ValidateDataAnnotations en
OptionsBuilder<TOptions> . Microsoft.Extensions.Options.DataAnnotations está incluido en el metapaquete
Microsoft.AspNetCore.App (ASP.NET Core 2.2 o versiones posteriores).

private class AnnotatedOptions


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

[StringLength(5, ErrorMessage = "Too long.")]


public string StringLength { get; set; }

[Range(-5, 5, ErrorMessage = "Out of range.")]


public int IntRange { get; set; }
}

[Fact]
public void CanValidateDataAnnotations()
{
var services = new ServiceCollection();
services.AddOptions<AnnotatedOptions>()
.Configure(o =>
{
o.StringLength = "111111";
o.IntRange = 10;
o.Custom = "nowhere";
})
.ValidateDataAnnotations();

var sp = services.BuildServiceProvider();

var error = Assert.Throws<OptionsValidationException>(() =>


sp.GetRequiredService<IOptionsMonitor<AnnotatedOptions>>().Value);
ValidateFailure<AnnotatedOptions>(error, Options.DefaultName, 1,
"DataAnnotation validation failed for members Required " +
"with the error 'The Required field is required.'.",
"DataAnnotation validation failed for members StringLength " +
"with the error 'Too long.'.",
"DataAnnotation validation failed for members IntRange " +
"with the error 'Out of range.'.");
}

La validación diligente (con respuesta rápida a errores en el inicio) se está teniendo en cuenta de cara a una
versión futura.

Configuración posterior de las opciones


Establezca la configuración posterior con IPostConfigureOptions<TOptions>. La configuración posterior se
ejecuta una vez completada toda la configuración de IConfigureOptions<TOptions>:
services.PostConfigure<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure está disponible para configurar posteriormente las opciones con nombre:

services.PostConfigure<MyOptions>("named_options_1", myOptions =>


{
myOptions.Option1 = "post_configured_option1_value";
});

Use PostConfigureAll para configurar posteriormente todas las instancias de configuración:

services.PostConfigureAll<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});

Acceso a opciones durante el inicio


IOptions<TOptions> y IOptionsMonitor<TOptions> puede usarse en Startup.Configure , ya que los servicios
se compilan antes de que se ejecute el método Configure .

public void Configure(IApplicationBuilder app, IOptionsMonitor<MyOptions> optionsAccessor)


{
var option1 = optionsAccessor.CurrentValue.Option1;
}

No use IOptions<TOptions> o IOptionsMonitor<TOptions> en 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
Usar varios entornos en ASP.NET Core
10/05/2019 • 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 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.UseStaticFiles();
app.UseMvc();
}

El código anterior:
Llama a UseDeveloperExceptionPage 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

El asistente de etiquetas de entorno usa el valor de IHostingEnvironment.EnvironmentName para incluir o


excluir el marcado en el elemento:
<environment include="Development">
<div>&lt;environment include="Development"&gt;</div>
</environment>
<environment exclude="Development">
<div>&lt;environment exclude="Development"&gt;</div>
</environment>
<environment include="Staging,Development,Staging_2">
<div>
&lt;environment include="Staging,Development,Staging_2"&gt;
</div>
</environment>

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:
IISExpress
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,
consulte Azure Documentation: Which settings are swapped? (Documentación de Azure: ¿qué opciones
de configuración se intercambian?).
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 Módulo ASP.NET Core.
Archivo del proyecto o perfil de publicación
Para las implementaciones de IIS de Windows: Incluya la propiedad <EnvironmentName> del perfil de
publicación (.pubxml) o un archivo de proyecto. Este método establece el entorno en web.config cuando se
publica el proyecto:

<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

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:
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.<Entorno>.json). Consulte Configuration: File configuration provider
(Configuración: proveedor de configuración de archivos).
Variables de entorno (establecidas en todos los sistemas donde se hospede la aplicación). Consulte
Configuration: File configuration provider (Configuración: proveedor de configuración de archivos) y
Safe storage of app secrets in development: Environment variables (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);
}

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.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

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.UseStaticFiles();
app.UseMvc();
}

public void ConfigureStaging(IApplicationBuilder app, IHostingEnvironment env)


{
if (!env.IsStaging())
{
throw new Exception("Not staging.");
}

app.UseExceptionHandler("/Error");
app.UseStaticFiles();
app.UseMvc();
}
}

Recursos adicionales
Inicio de la aplicación en ASP.NET Core
Configuración en ASP.NET Core
IHostingEnvironment.EnvironmentName
Registro en ASP.NET Core
05/07/2019 • 50 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 gran variedad de proveedores de
registro integrados y de terceros. En este artículo se muestra cómo usar las API de registro con proveedores
integrados.
Vea o descargue el código de ejemplo (cómo descargarlo)

Incorporación de proveedores
Un proveedor de registro muestra o almacena registros. Por ejemplo, el proveedor de consola muestra los registros
en la consola y el proveedor de Azure Application Insights los almacena en Azure Application Insights. Los
registros se pueden enviar a varios destinos mediante la incorporación de varios proveedores.
Para usar un proveedor, llame al método de extensión Add{provider name} 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) =>
{
// Requires `using Microsoft.Extensions.Logging;`
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.UseStartup<Startup>()
.Build();

webHost.Run();
}

El código anterior requiere referencias a Microsoft.Extensions.Logging y Microsoft.Extensions.Configuration .


La plantilla de proyecto predeterminada llama a CreateDefaultBuilder, que agrega los siguientes proveedores de
registro:
Consola
Depuración
EventSource (a partir de ASP.NET Core 2.2)
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

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


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

Si usa CreateDefaultBuilder , puede reemplazar los proveedores predeterminados por sus propios valores. Llame a
ClearProviders y agregue los proveedores que desee.

public static void Main(string[] args)


{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});

Para usar un proveedor, instale su paquete NuGet y llame al método de extensión del proveedor en una instancia
de ILoggerFactory:

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 agrega proveedores de registro en el método Startup.Configure . Para 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 los proveedores de registro de terceros
más adelante en el artículo.
Creación de registros
Obtenga un objeto ILogger<TCategoryName> a partir de la inserción de dependencias.
El siguiente ejemplo de controlador crea los registros Information y Warning . La categoría es
TodoApiSample.Controllers.TodoController (el nombre de clase completo de TodoController en la aplicación de
ejemplo):

public class TodoController : Controller


{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

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 siguiente ejemplo de Razor Pages crea registros con Information como el nivel y
TodoApiSample.Pages.AboutModel como la categoría:

public class AboutModel : PageModel


{
private readonly ILogger _logger;

public AboutModel(ILogger<AboutModel> logger)


{
_logger = logger;
}

public void OnGet()


{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation("Message displayed: {Message}", Message);
}
public class TodoController : Controller
{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

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 ejemplo anterior crea registros con Information y Warning como el nivel y la clase TodoController como la
categoría.
El nivel de registro indica la gravedad del evento registrado. La categoría de registro es una cadena que está
asociada con cada registro. La ILogger<T> instancia crea registros que tienen el nombre completo del tipo T como
la categoría. Los niveles y las categorías se explican detalladamente más adelante en este artículo.
Creación de registros durante el inicio
Para escribir registros en la clase Startup , incluya un parámetro ILogger en la signatura de construcción:
public class Startup
{
private readonly ILogger _logger;

public Startup(IConfiguration configuration, ILogger<Startup> logger)


{
Configuration = configuration;
_logger = logger;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

// Add our repository type


services.AddSingleton<ITodoRepository, TodoRepository>();
_logger.LogInformation("Added TodoRepository to services");
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
_logger.LogInformation("In Development environment");
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseMvc();
}
}

Creación de registros en la clase Program


Para escribir registros la clase Program , obtenga una instancia ILogger de inserción de dependencias:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});

No hay métodos de registrador asincrónicos


El registro debe ser tan rápido que no merezca la pena el costo de rendimiento del código asincrónico. Si el
almacén de datos de registro es lento, no escriba directamente en él. Considere la posibilidad de escribir los
mensajes de registro en un almacén rápido inicialmente y luego moverlos a la tienda lenta. Por ejemplo, si inicia
sesión en SQL Server, no desea hacerlo directamente en un método Log , ya que los métodos Log son
sincrónicos. En su lugar, agregue sincrónicamente mensajes de registro a una cola en memoria y haga que un
trabajo en segundo plano extraiga los mensajes de la cola para realizar el trabajo asincrónico de insertar datos en
SQL Server.

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

La propiedad Logging puede tener LogLevel y propiedades del proveedor de registro (se muestra la consola).
La propiedad LogLevel bajo Logging especifica el nivel mínimo que se va a registrar para las categorías
seleccionadas. En el ejemplo, las categorías System y Microsoft se registran en el nivel Information y todas las
demás se registran en el nivel Debug .
Otras propiedades bajo Logging especifican proveedores de registro. El ejemplo es para el proveedor de consola.
Si un proveedor admite ámbitos de registro, IncludeScopes indica si están habilitados. Una propiedad de
proveedor (como Console en el ejemplo) también puede especificar una propiedad LogLevel . LogLevel en un
proveedor especifica niveles de registro para ese proveedor.
Si los niveles se especifican en Logging.{providername}.LogLevel , invalidan todo lo establecido en Logging.LogLevel
.

{
"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, los registros aparecen en la consola cuando la
aplicación se ejecuta 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

Los registros anteriores se generaron mediante la realización de una solicitud HTTP GET a la aplicación de ejemplo
en http://localhost:5000/api/todo/0 .
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 del código de
marco de ASP.NET Core. 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
Cuando se crea un objeto ILogger , se ha especificado una categoría para él. Esa categoría se incluye con cada
mensaje de registro creado por esa instancia de ILogger . La categoría puede ser cualquier cadena, pero la
convención es usar el nombre de clase, como "TodoApi.Controllers.TodoController".
Use ILogger<T> para obtener una instancia ILogger que utiliza el nombre de tipo completo de T como la
categoría:
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;
}

Para especificar explícitamente la categoría, llame a ILoggerFactory.CreateLogger :

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

ILogger<T> es equivale a llamar a CreateLogger con el nombre de tipo completo de T .

Nivel de registro
Cara registro especifica un valor LogLevel. El nivel de registro indica la gravedad o importancia. Por ejemplo,
podría escribir un registro Information cuando un método termina con normalidad y un registro Warning cuando
un método devuelve un código de estado 404 No encontrado.
El siguiente código crea los registros Information y Warning :
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 el código anterior, el primer parámetro es el id. de 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 en la sección de la plantilla de mensaje más
adelante en este artículo.
Los métodos de registro que incluyen el nivel en el nombre del método (por ejemplo LogInformation y LogWarning
) son métodos de extensión para ILogger. 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 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 información que normalmente solo es útil para la depuración. 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.
Depurar = 1
Para información que puede ser útil para el desarrollo y la depuración. Ejemplo:
Entering method Configure with flag set to true. Habilite los registros de nivel Debug en producción
cuando 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.
Use 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, envíe Trace a través del nivel Information a un almacén de datos de volumen. Envíe Warning a
través de Critical a un almacén de datos de valor.
Durante el desarrollo, envíe Warning a través de Critical a la consola y agregue Trace a través de
Information cuando solucione problemas.

En la sección Filtrado del registro de este artículo se explica cómo controlar los niveles de registro que controla un
proveedor.
ASP.NET Core escribe registros de eventos de marco. En los ejemplos de registro anteriores de este artículo se
excluyeron los registros por debajo del nivel Information , por lo que no se crearon los registros de nivel Debug o
Trace . Este es un ejemplo de registros de consola generados mediante la ejecución de la aplicación de ejemplo
configurada para mostrar registros Debug :

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 registro se 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 id. de evento asocia un conjunto de eventos. Por ejemplo, todos los registros relacionados con la presentación
de una lista de elementos en una página podrían ser 1001.
El proveedor de registro puede almacenar el id. de evento en un campo de identificador, en el mensaje de registro o
no almacenarlo. El proveedor de depuración no muestra los identificadores de evento. El proveedor de consola
muestra los identificadores de evento entre corchetes 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 registro especifica una plantilla de mensaje. La plantilla de mensaje puede contener marcadores de posición
para los que se proporcionan argumentos. Use los nombres de los marcadores de posición, no números.

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. En el código siguiente, tenga en cuenta que los nombres de parámetro están fuera de la secuencia en la
plantilla de mensaje:

string p1 = "parm1";
string p2 = "parm2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);

Este código crea un mensaje de registro con los valores de parámetro en secuencia:

Parameter values: parm1, parm2

La plataforma de registro funciona de esta manera para que los proveedores de registro puedan implementar el
registro semántico, también conocido como registro estructurado. Los propios argumentos se pasan al sistema de
registro, no solo a la plantilla de mensaje con formato. Esta información permite a los proveedores de registro
almacenar los valores de parámetro como campos. Por ejemplo, suponga que las llamadas del método del
registrador tiene el aspecto siguiente:
_logger.LogInformation("Getting item {ID} at {RequestTime}", id, DateTime.Now);

Si envía los registros a Azure Table Storage, cada entidad de Azure Table puede tener propiedades ID y
RequestTime , lo que simplifica las consultas en los datos de registro. Una consulta puede buscar todos los registros
dentro de un intervalo RequestTime determinado sin 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.
Para suprimir todos los registros, especifique 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


El código de la plantilla de proyecto 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) =>
{
// Requires `using Microsoft.Extensions.Logging;`
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.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 para todos los proveedores. Se elige una sola regla para cada proveedor cuando se crea un objeto
ILogger .

Reglas de filtro en el código


En el siguiente ejemplo se muestra cómo registrar reglas de filtro en el código:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace));

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 COMIENZAN


NÚMERO PROVEEDOR POR... NIVEL DE REGISTRO MÍNIMO

1 Depuración Todas las categorías Información

2 Consola Microsoft.AspNetCore.Mvc.R Advertencia


azor.Internal

3 Consola Microsoft.AspNetCore.Mvc.R Depuración


azor.Razor

4 Consola Microsoft.AspNetCore.Mvc.R Error


azor

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 , el objeto ILoggerFactory selecciona una sola regla por proveedor para aplicar
a ese registrador. Todos los mensajes escritos por una instancia 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
coincidencia, 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 coincidencia, 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 .
Con la lista de reglas anterior, supongamos que 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.
La instancia ILogger resultante envía los registros de nivel Trace y superiores al proveedor de depuración. Los
registros de nivel Debug y superiores se envían al proveedor de consola.
Alias de proveedor
Cada proveedor define un alias que se puede utilizar en la configuración en lugar del nombre de tipo completo.
Para los proveedores integrados, use los alias siguientes:
Consola
Depuración
EventSource
EventLog
TraceSource
AzureAppServicesFile
AzureAppServicesBlob
ApplicationInsights
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));

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
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, la categoría y el nivel de registro.
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;
});
});

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 aceptan 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.
Para establecer reglas de filtrado para todos los proveedores que están registrados con un instancia de
ILoggerFactory , use 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 los registros creados por
el código de aplicación se registran 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();

Para evitar que los registros se escriban, especifique LogLevel.None como el nivel de registro mínimo. 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.

Niveles y categorías del sistema


Estas son algunas categorías que ASP.NET Core y Entity Framework Core usan, con notas sobre lo que los
registros de espera de ellas:

CATEGORÍA NOTAS

Microsoft.AspNetCore Diagnósticos generales de ASP.NET Core.

Microsoft.AspNetCore.DataProtection Qué claves se tuvieron en cuenta, encontraron y usaron.

Microsoft.AspNetCore.HostFiltering Hosts permitidos.

Microsoft.AspNetCore.Hosting Cuánto tiempo tardaron en completarse las solicitudes HTTP y


a qué hora comenzaron. Qué ensamblados de inicio de
hospedaje se cargaron.
CATEGORÍA NOTAS

Microsoft.AspNetCore.Mvc Diagnósticos de MVC y Razor. Enlace de modelos, ejecución de


filtros, compilación de vistas y selección de acciones.

Microsoft.AspNetCore.Routing Información de coincidencia de ruta.

Microsoft.AspNetCore.Server Inicio y detención de conexión y mantener las respuestas


activas. Información de certificado HTTPS.

Microsoft.AspNetCore.StaticFiles Archivos servidos.

Microsoft.EntityFrameworkCore Diagnósticos generales de Entity Framework Core. Actividad y


la configuración de bases de datos, detección de cambios y
migraciones.

Ámbitos de registro
Un ámbito puede agrupar un conjunto de operaciones lógicas. Esta agrupación se puede utilizar para adjuntar los
mismos datos para cada registro que se crea como parte de un conjunto. Por ejemplo, cada registro creado como
parte del procesamiento de una transacción puede incluir el identificador de dicha transacción.
Un ámbito es un tipo IDisposable devuelto por el método BeginScope y se conserva hasta que se elimina. Use un
ámbito encapsulando las llamadas de registrador en un bloque using :

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
AzureAppServicesFile
AzureAppServicesBlob
ApplicationInsights
Para información sobre el registro de stdout, consulte Solución de problemas de ASP.NET Core en IIS y Solución
de problemas de ASP.NET Core en 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.
El proveedor de consola tiene un impacto importante en el rendimiento y no suele ser adecuado para su uso en
producción.
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.
Para ver una salida de registro de la consola, abra un símbolo del sistema en la carpeta del proyecto y ejecute este
comando:

dotnet run

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
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
puede 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.
El paquete de proveedor no está incluido en el metapaquete Microsoft.AspNetCore.App. Para usar el proveedor,
instale el paquete.
Si el destino es .NET Framework o hace referencia al metapaquete Microsoft.AspNetCore.App , agregue el paquete
de proveedor al proyecto. Invoke AddAzureWebAppDiagnostics :

logging.AddAzureWebAppDiagnostics();

loggerFactory.AddAzureWebAppDiagnostics();

Una sobrecarga AddAzureWebAppDiagnostics permite pasar AzureAppServicesDiagnosticsSettings. El objeto de


configuración puede invalidar la configuración predeterminada, como la plantilla de salida de registro, el nombre
de blob y el límite de tamaño de archivo. (La plantilla salida es una plantilla de mensaje que se aplica a todos los
registros además de que se proporciona con una llamada de método ILogger ).
Para configurar las opciones de proveedor, use AzureFileLoggerOptions y AzureBlobLoggerOptions, tal y como se
muestra en el ejemplo siguiente:

public static void Main(string[] args)


{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddAzureWebAppDiagnostics())
.ConfigureServices(serviceCollection => serviceCollection
.Configure<AzureFileLoggerOptions>(options =>
{
options.FileName = "azure-diagnostics-";
options.FileSizeLimit = 50 * 1024;
options.RetainedFileCountLimit = 5;
}).Configure<AzureBlobLoggerOptions>(options =>
{
options.BlobName = "log.txt";
}))
.UseStartup<Startup>();

Al realizar una implementación en una aplicación de App Service, esta respeta la configuración de la sección
Registros de App Service de la página App Service de Azure Portal. Cuando se actualiza la configuración
siguiente, los cambios se aplican de inmediato sin necesidad de reiniciar ni de volver a implementar la aplicación.
Registro de la aplicación (sistema de archivos)
Registro de la aplicación (blob)
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.
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).
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 App Service desde la página de portal de la aplicación.
Establezca Registro de la aplicación (sistema de archivos) en Activado.
Elija el Nivel de registro.
Navegue hasta la página Secuencia de registro para consultar los mensajes de la aplicación. La aplicación los
registra a través de la interfaz ILogger .
Registro de seguimiento de Azure Application Insights
El proveedor de paquete Microsoft.Extensions.Logging.ApplicationInsights escribe los registros en Azure
Application Insights. Application Insights es un servicio que supervisa una aplicación web y proporciona
herramientas para consultar y analizar los datos de telemetría. Si usa este proveedor, puede consultar y analizar los
registros mediante las herramientas de Application Insights.
El proveedor de registro se incluye como dependencia de Microsoft.ApplicationInsights.AspNetCore, que es el
paquete que proporciona toda la telemetría disponible para ASP.NET Core. Si usa este paquete, no tiene que
instalar el proveedor de paquete.
No use el paquete Microsoft.ApplicationInsights.Web —que es para ASP.NET 4.x.
Para obtener más información, vea los siguientes recursos:
Información general de Application Insights
Application Insights para aplicaciones de ASP.NET Core: comience aquí si quiere implementar la variedad
completa de telemetría de Application Insights junto con el registro.
ApplicationInsightsLoggerProvider para los registros de .NET Core ILogger: comience aquí si quiere
implementar el proveedor de registro sin el resto de la telemetría de Application Insights.
Adaptadores de registro de Application Insights
Instalación, configuración e inicialización del SDK de Application Insights: tutorial interactivo en el sitio de
Microsoft Learn.

NOTE
A partir del 1 de mayo de 2019, el artículo titulado Application Insights para ASP.NET Core está desactualizado y los pasos del
tutorial no funcionan. Consulte en su lugar Application Insights para aplicaciones de ASP.NET Core. Somos conscientes del
problema y estamos trabajando para corregirlo.

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)
Sentry (repositorio de GitHub)
Serilog (repositorio de GitHub)
Stackdriver (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 ILoggerFactory .
Para más información, vea la documentación de cada proveedor. Microsoft no admite los proveedores de registro
de terceros.

Recursos adicionales
Registro de alto rendimiento con LoggerMessage en ASP.NET Core
Enrutamiento en ASP.NET Core
03/07/2019 • 74 minutes to read • Edit Online

Por Ryan Nowak, Steve Smith y Rick Anderson


Para obtener la versión 1.1 de este tema, descargue Routing in ASP.NET Core (version 1.1, PDF )
[Enrutamiento en ASP.NET Core (versión 1.1, PDF )].
El enrutamiento es responsable de asignar URI de solicitud a los selectores de punto de conexión y de
distribuir las solicitudes entrantes a los puntos de conexión. 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, el enrutamiento también puede generar direcciones URL que se asignan a selectores de punto de
conexión.
Para usar los escenarios de enrutamiento más recientes de ASP.NET Core 2.2, especifique la versión de
compatibilidad en el registro de servicios de MVC en Startup.ConfigureServices :

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

La opción EnableEndpointRouting determina si el enrutamiento debe usar de forma interna la lógica basada
en el punto de conexión o la lógica basada en IRouter de ASP.NET Core 2.1 o una versión anterior. Cuando
la versión de compatibilidad se establece en 2.2 o una versión posterior, el valor predeterminado es true .
Establezca el valor en false para usar la lógica de enrutamiento anterior:

// Use the routing logic of ASP.NET Core 2.1 or earlier:


services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Para obtener más información sobre el enrutamiento basado en IRouter, vea la versión para ASP.NET Core
2.1 de este tema.
El enrutamiento es responsable de asignar los URI de solicitud a los controladores de ruta y de distribuir las
solicitudes entrantes. 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. Mediante las rutas configuradas de la aplicación, el enrutamiento puede
generar direcciones URL que se asignan a los controladores de ruta.
Para usar los escenarios de enrutamiento más recientes de ASP.NET Core 2.1, especifique la versión de
compatibilidad en el registro de servicios de MVC en Startup.ConfigureServices :

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
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. Para obtener más
información sobre las convenciones de enrutamiento en Razor Pages, consulte Convenciones de aplicación y de ruta
de páginas de Razor en ASP.NET Core.

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

Fundamentos del enrutamiento


La mayoría de las aplicaciones deben elegir un esquema de enrutamiento básico y descriptivo para que las
direcciones URL sean legibles y significativas. La ruta convencional predeterminada
{controller=Home}/{action=Index}/{id?} :

Admite un esquema de enrutamiento básico y descriptivo.


Se trata de un punto de partida útil para las aplicaciones basadas en la interfaz de usuario.
Los desarrolladores suelen agregar rutas breves adicionales a áreas de mucho tráfico de una aplicación en
situaciones especializadas (por ejemplo, puntos de conexión de blog y comercio electrónico) con el
enrutamiento mediante atributos o rutas convencionales dedicadas.
Las API web deben usar el enrutamiento mediante atributos para modelar la funcionalidad de la aplicación
como un conjunto de recursos donde las operaciones se representan mediante verbos HTTP. Esto significa
que muchas operaciones (por ejemplo, GET y POST) del mismo recurso lógico usarán la misma dirección
URL. El enrutamiento mediante atributos proporciona un nivel de control que es necesario para diseñar
cuidadosamente un diseño de puntos de conexión públicos de la API.
En las aplicaciones de Razor Pages se usa el enrutamiento convencional predeterminado para proporcionar
recursos con nombre en la carpeta Páginas de una aplicación. Existen convenciones adicionales que
permiten personalizar el comportamiento de enrutamiento de Razor Pages. Para obtener más información,
vea Introducción a las páginas de Razor en ASP.NET Core y Convenciones de aplicación y de ruta de
páginas de Razor en ASP.NET Core.
La compatibilidad de la generación de direcciones URL permite desarrollar la aplicación sin codificar de
forma rígida las direcciones URL para vincular la aplicación. Esta compatibilidad permite empezar con una
configuración de enrutamiento básica y modificar las rutas una vez determinado el diseño de los recursos de
la aplicación.
El enrutamiento usa puntos de conexión ( Endpoint ) para representar los puntos de conexión lógicos en una
aplicación.
Un punto de conexión define un delegado para procesar las solicitudes y una colección de metadatos
arbitrarios. Los metadatos se usan para implementar cuestiones transversales según las directivas y la
configuración asociada a cada punto de conexión.
El sistema de enrutamiento tiene las características siguientes:
La sintaxis de plantilla de ruta se usa para definir las rutas con parámetros de ruta con tokens.
Se permite la configuración de puntos de conexión de estilo convencional y de estilo de atributo.
Se usa IRouteConstraint para determinar si un parámetro de dirección URL contiene un valor válido
para una restricción de punto de conexión determinada.
Los modelos de aplicación, como MVC y Razor Pages, registran todos sus puntos de conexión, que
presentan una implementación predecible de los escenarios de enrutamiento.
La implementación de enrutamiento toma decisiones relativas al enrutamiento siempre que sea lo
deseado en la canalización de middleware.
El middleware que aparece después de un middleware de enrutamiento puede inspeccionar el
resultado de la decisión del punto de conexión del middleware de enrutamiento para un URI de
solicitud determinado.
Se pueden enumerar todos los puntos de conexión de la aplicación en cualquier parte de la
canalización de middleware.
Una aplicación puede usar el enrutamiento para generar direcciones URL (por ejemplo, para el
redireccionamiento o los vínculos) en función de la información del punto de conexión. De este modo,
se evita codificar de forma rígida las direcciones URL, lo que facilita el mantenimiento.
La generación de direcciones URL se basa en direcciones, que admiten la extensibilidad arbitraria:
La API del generador de vínculos (LinkGenerator) se puede resolver en cualquier lugar mediante
la inserción de dependencias (DI) para generar direcciones URL.
Cuando la API del generador de vínculos no está disponible a través de DI, IUrlHelper ofrece
métodos para generar direcciones URL.

NOTE
Con el lanzamiento del enrutamiento de punto de conexión en ASP.NET Core 2.2, la vinculación de punto de conexión
se limita a acciones y páginas de Razor Pages y MVC. Las expansiones de las funciones de vinculación de punto de
conexión están previstas para próximas versiones.

El enrutamiento usa rutas (implementaciones de IRouter) para:


Asignar las solicitudes entrantes a controladores de ruta.
Generar las direcciones URL que se usan en las respuestas.
De forma predeterminada, una aplicación tiene una sola colección de rutas. Cuando llega una solicitud, las
rutas de la colección se procesan en el orden en el que se encuentran en la colección. El marco de trabajo
intenta hacer coincidir una dirección URL de solicitud entrante con una ruta de la colección mediante una
llamada al método RouteAsync en cada ruta de la colección. 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 codificar de forma rígida las direcciones URL, lo que facilita el
mantenimiento.
El sistema de enrutamiento tiene las características siguientes:
La sintaxis de plantilla de ruta se usa para definir las rutas con parámetros de ruta con tokens.
Se permite la configuración de puntos de conexión de estilo convencional y de estilo de atributo.
Se usa IRouteConstraint para determinar si un parámetro de dirección URL contiene un valor válido para
una restricción de punto de conexión determinada.
Los modelos de aplicación, como MVC y Razor Pages, registran todas sus rutas, que tienen una
implementación predecible de los escenarios de enrutamiento.
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 codificar
de forma rígida las direcciones URL, lo que facilita el mantenimiento.
La generación de direcciones URL se basa en rutas, que admiten la extensibilidad arbitraria. IUrlHelper
ofrece métodos para generar direcciones URL.
La clase RouterMiddleware conecta el enrutamiento a la canalización de software intermedio. ASP.NET Core
MVC agrega enrutamiento a la canalización de middleware como parte de su configuración y controla el
enrutamiento en las aplicaciones de MVC y Razor Pages. Para obtener información sobre cómo usar el
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
punto de conexión. Este proceso se basa 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.
Cuando se ejecuta un middleware de enrutamiento, se establece un punto de conexión ( Endpoint ) y se
enrutan los valores a una característica de HttpContext. Para la solicitud actual:
Al llamar a HttpContext.GetEndpoint se obtiene el punto de conexión.
HttpRequest.RouteValues obtiene la colección de valores de ruta.

El middleware que se ejecuta después del middleware de enrutamiento puede ver el punto de conexión y
tomar medidas. Por ejemplo, un middleware de autorización puede consultar la colección de metadatos del
punto de conexión de una directiva de autorización. Después de que se ejecuta todo el middleware en la
canalización de procesamiento de solicitudes, se invoca al delegado del punto de conexión seleccionado.
El sistema de enrutamiento en el enrutamiento de punto de conexión es responsable de todas las decisiones
relativas al envío. Como el middleware aplica las directivas en función del punto de conexión seleccionado,
es importante que cualquier decisión que pueda afectar a la distribución o la aplicación de directivas de
seguridad se realice dentro del sistema de enrutamiento.
Cuando se ejecuta el delegado del punto de conexión, las propiedades de RouteContext.RouteData se
establecen en los valores adecuados en función del procesamiento de solicitudes realizado hasta el
momento.
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 basa 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 no
se encuentra ningún controlador de ruta para procesar la solicitud, el middleware entrega la solicitud al
siguiente middleware en 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 que llama a RouteAsync también establece las propiedades de RouteContext.RouteData en
los valores adecuados en función del procesamiento de solicitudes realizado hasta el momento.
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 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 RouteData.DataTokens pueden ser de cualquier tipo, a
diferencia de RouteData.Values, 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 direcciones URL con LinkGenerator
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 puntos de
conexión y las direcciones URL que tienen acceso a ellos.
El enrutamiento de punto de conexión incluye la API del generador de vínculos (LinkGenerator).
LinkGenerator es un servicio singleton que se puede recuperar a partir de la DI. La API se puede usar fuera
del contexto de una solicitud en ejecución. IUrlHelper de MVC y los escenarios que dependen de IUrlHelper,
como los asistentes de etiquetas, los de HTML y los resultados de acción, usan el generador de vínculos para
proporcionar funciones de generación de vínculos.
El generador de vínculos está respaldado por el concepto de una dirección y esquemas de direcciones. Un
esquema de direcciones es una manera de determinar los puntos de conexión que se deben tener en cuenta
para la generación de vínculos. Por ejemplo, los escenarios de nombre y valores de ruta de Razor Pages y
MVC con los que muchos usuarios están familiarizados se implementan como un esquema de direcciones.
El generador de vínculos puede vincular a acciones y páginas de Razor Pages y MVC a través de los métodos
de extensión siguientes:
GetPathByAction
GetUriByAction
GetPathByPage
GetUriByPage
Una sobrecarga de estos métodos acepta argumentos que incluyan HttpContext . Estos métodos son
equivalentes funcionalmente a Url.Action y Url.Page , pero ofrecen flexibilidad y opciones adicionales.
Los métodos GetPath* son más similares a Url.Action y Url.Page , dado que generan un URI que contiene
una ruta de acceso absoluta. Los métodos GetUri* siempre generan un URI absoluto que contiene un
esquema y un host. Los métodos que aceptan HttpContext generan un URI en el contexto de la solicitud que
se ejecuta. A menos que se reemplacen, se usan los valores de ruta de ambiente, la ruta de acceso base de la
dirección URL, el esquema y el host de la solicitud que se ejecuta.
Se llama a LinkGenerator con una dirección. La generación de un URI se produce en dos pasos:
1. Se enlaza una dirección a una lista de puntos de conexión que coincidan con la dirección.
2. Se evalúa el elemento RoutePattern de cada punto de conexión hasta que se encuentra un patrón de ruta
que coincida con los valores proporcionados. La salida resultante se combina con otras partes del URI
proporcionadas al generador de vínculos y devueltas.
Los métodos proporcionados por LinkGenerator admiten funciones estándar de generación de vínculos para
cualquier tipo de dirección. La forma más útil de usar el generador de vínculos es a través de métodos de
extensión que realicen operaciones para un tipo de dirección específica.
MÉTODO DE EX TENSIÓN DESCRIPCIÓN

GetPathByAddress Genera un URI con una ruta de acceso absoluta en función


de los valores proporcionados.

GetUriByAddress Genera un URI absoluto en función de los valores


proporcionados.

WARNING
Preste atención a las consecuencias siguientes de llamar a los métodos LinkGenerator:
Use los métodos de extensión GetUri* con precaución en una configuración de aplicación en la que no se
valide el encabezado Host de las solicitudes entrantes. Si no se valida el encabezado Host de las solicitudes
entrantes, la entrada de la solicitud que no sea de confianza se puede devolver al cliente en los URI de una
página o vista. Se recomienda que todas las aplicaciones de producción configuren su servidor para validar el
encabezado Host en función de valores válidos conocidos.
Use LinkGenerator con precaución en el middleware junto con Map o MapWhen . Map* cambia la ruta de
acceso base de la solicitud que se ejecuta, lo que afecta a la salida de la generación de vínculos. Todas las API
de LinkGenerator permiten especificar una ruta de acceso base. Especifique siempre una ruta de acceso base
vacía para deshacer el efecto de Map* en la generación de vínculos.

Diferencias con respecto a versiones anteriores del enrutamiento


Existen algunas diferencias entre el enrutamiento de punto de conexión de ASP.NET Core 2.2 o posterior, y
las versiones anteriores del enrutamiento en ASP.NET Core:
El sistema de enrutamiento de punto de conexión no es compatible con la extensibilidad basada en
IRouter, incluida la herencia de Route.
El enrutamiento de punto de conexión no es compatible con WebApiCompatShim. Utilice la versión
compatibilidad 2.1 ( .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) ) para seguir usando
la corrección de compatibilidad.
El enrutamiento de punto de conexión tiene un comportamiento diferente para el uso de mayúsculas
y minúsculas en los URI generados al usar rutas convencionales.
Tenga en cuenta la plantilla de ruta predeterminada siguiente:

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

Supongamos que genera un vínculo a una acción mediante la ruta siguiente:

var link = Url.Action("ReadPost", "blog", new { id = 17, });

Con el enrutamiento basado en IRouter, este código genera un URI de /blog/ReadPost/17 , que
respeta las mayúsculas y minúsculas del valor de ruta proporcionado. El enrutamiento de punto de
conexión de ASP.NET Core 2.2 o versiones posteriores genera /Blog/ReadPost/17 ("Blog" se pone
mayúscula). El enrutamiento de punto de conexión proporciona la interfaz
IOutboundParameterTransformer , que se puede usar para personalizar este comportamiento de forma
global, o bien para aplicar otras convenciones para la asignación de direcciones URL.
Para obtener más información, vea la sección Referencia de transformadores de parámetros.
La generación de vínculos que se usa en Razor Pages y MVC con las rutas convencionales tiene un
comportamiento diferente al intentar vincularlo a un controlador o una acción, o a una página que no
existe.
Tenga en cuenta la plantilla de ruta predeterminada siguiente:

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

Supongamos que genera un vínculo a una acción mediante la plantilla predeterminada con lo
siguiente:

var link = Url.Action("ReadPost", "Blog", new { id = 17, });

Con el enrutamiento basado en IRouter , el resultado siempre es /Blog/ReadPost/17 , incluso aunque


BlogController no exista o no tenga un método de acción ReadPost . Como se esperaba, si el método
de acción existe, el enrutamiento de punto de conexión de ASP.NET Core 2.2 o versiones posteriores
genera /Blog/ReadPost/17 . Sin embargo, si la acción no existe, el enrutamiento de punto de conexión
genera una cadena vacía. Conceptualmente, el enrutamiento de punto de conexión no supone que, si
la acción no existe, sí que exista el punto de conexión.
El algoritmo de invalidación del valor de ambiente de la generación de vínculos tiene otro
comportamiento al usarlo con el enrutamiento de punto de conexión.
La invalidación del valor de ambiente es el algoritmo que decide qué valores de ruta de la solicitud
que se ejecuta actualmente (los valores de ambiente) se pueden usar en las operaciones de
generación de vínculos. El enrutamiento convencional siempre invalida los valores de ruta adicionales
al vincular a otra acción. El enrutamiento mediante atributos no tenía este comportamiento antes del
lanzamiento de ASP.NET Core 2.2. En versiones anteriores de ASP.NET Core, los vínculos a otra
acción que usara los mismos nombres de parámetro de ruta producían errores de generación de
vínculo. En ASP.NET Core 2.2 o versiones posteriores, las dos formas de enrutamiento invalidan los
valores cuando se vincula a otra acción.
Considere el ejemplo siguiente de ASP.NET Core 2.1 o una versión anterior. Al vincular a otra acción
(o a otra página), los valores de ruta se pueden reutilizar de formas no deseadas.
En /Pages/Store/Product.cshtml:

@page "{id}"
@Url.Page("/Login")

En /Pages/Login.cshtml:

@page "{id?}"

Si el URI es /Store/Product/18en ASP.NET Core 2.1 o versiones anteriores, el vínculo que genera
@Url.Page("/Login") en la página Store/Info es /Login/18 . Se reutiliza el valor id de 18, aunque el
destino del vínculo sea otro elemento totalmente distinto de la aplicación. El valor de ruta id en el
contexto de la página /Login probablemente sea un valor de id. de usuario, no un valor de id. de
producto de tienda.
En el enrutamiento de punto de conexión con ASP.NET Core 2.2 o versiones posteriores, el resultado
es /Login . Los valores de ambiente no se reutilizan cuando el destino vinculado es otra acción o
página.
Sintaxis de parámetro de ruta de ida y vuelta: las barras diagonales no se codifican cuando se usa una
sintaxis de parámetro comodín de doble asterisco ( ** ).
Durante la generación de vínculos, el sistema de enrutamiento codifica el valor capturado en un
parámetro comodín de doble asterisco ( ** ; por ejemplo, {**myparametername} ), excepto las barras
diagonales. El comodín de doble asterisco es compatible con el enrutamiento basado en IRouter en
ASP.NET Core 2.2 o versiones posteriores.
La sintaxis de parámetro comodín de un único asterisco en versiones anteriores de ASP.NET Core (
{*myparametername} ) sigue siendo compatible y las barras diagonales se codifican.

VÍNCULO GENERADO CON


RUTA URL.ACTION(NEW { CATEGORY = "ADMIN/PRODUCTS" }) …

/search/{*page} /search/admin%2Fproducts (la barra diagonal se


codifica)

/search/{**page} /search/admin/products

Ejemplo de middleware
En el ejemplo siguiente, un middleware usa la API LinkGenerator para crear el vínculo a un método de
acción que enumera los productos de la tienda. El uso del generador de vínculos mediante su inserción en
una clase y la llamada a GenerateLink está disponible para cualquier clase de una aplicación.

using Microsoft.AspNetCore.Routing;

public class ProductsLinkMiddleware


{
private readonly LinkGenerator _linkGenerator;

public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)


{
_linkGenerator = linkGenerator;
}

public async Task InvokeAsync(HttpContext httpContext)


{
var url = _linkGenerator.GetPathByAction("ListProducts", "Store");

httpContext.Response.ContentType = "text/plain";

await httpContext.Response.WriteAsync($"Go to {url} to see our products.");


}
}

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
de ruta 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. 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 si
es posible generar una dirección URL y qué valores se van a incluir. AmbientValues son el conjunto de
valores de ruta producidos a partir de la coincidencia con la solicitud actual. 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 debe 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 generar direcciones URL
para los vínculos con la misma ruta o valores de ruta.

La salida de GetVirtualPath es VirtualPathData. VirtualPathData es un valor paralelo de RouteData.


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 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 los métodos de extensión IRouteBuilder crean una
instancia de Route y la agregan a la colección de rutas.
MapRoute no acepta un parámetro de controlador de ruta. MapRoute solo agrega las rutas que se controlan
mediante DefaultHandler. Para obtener más información sobre el enrutamiento en MVC, vea Enrutar a
acciones de controlador de ASP.NET Core.
MapRoute no acepta un parámetro de controlador de ruta. MapRoute solo agrega las rutas que se controlan
mediante DefaultHandler. El controlador predeterminado es un elemento IRouter , y es posible que el
controlador no pueda atender 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 en 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 y extrae los valores de ruta. Por ejemplo, la ruta de
acceso /Products/Details/17 genera los valores de ruta siguientes:
{ 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 definir los parámetros, 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 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 un parámetro 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 la sección Referencia de plantilla de ruta para obtener una descripción
detallada de los escenarios y la sintaxis de la plantilla de ruta.
En el ejemplo siguiente, la definición de parámetro de ruta {id:int} define una restricción de ruta para el
parámetro de ruta id :

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 . 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 la Referencia de restricción de ruta para obtener una explicación de las restricciones de ruta
proporcionadas por el marco de trabajo.
Las sobrecargas adicionales de MapRoute aceptan valores para constraints , dataTokens y defaults . Estos
parámetros suelen usarse para pasar un objeto de tipo anónimo, donde los nombres de propiedad del tipo
anónimo coinciden con los nombres de los parámetros de ruta.
En los ejemplos MapRoute 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 algunos escenarios, como los tokens de datos, que no son compatibles con la sintaxis insertada.

En el ejemplo siguiente se muestran algunos escenarios más:

routes.MapRoute(
name: "blog",
template: "Blog/{**article}",
defaults: new { controller = "Blog", action = "ReadArticle" });

La plantilla anterior 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 doble ( ** )
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.

routes.MapRoute(
name: "blog",
template: "Blog/{*article}",
defaults: new { controller = "Blog", action = "ReadArticle" });

La plantilla anterior 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 el ejemplo siguiente 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" });

La plantilla anterior 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 direcciones URL de clase de ruta
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 el ejemplo siguiente se usa una ruta predeterminada de ASP.NET Core MVC general:

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

Con los valores de ruta { controller = Products, action = List } , se 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. Como id es un parámetro de ruta opcional, la dirección URL se ha generado
correctamente sin ningún valor para id .
Con los valores de ruta { controller = Home, action = Index } , se genera la dirección URL / . Los valores
de ruta proporcionados coinciden con los valores predeterminados, y los segmentos correspondientes a los
valores predeterminados se omiten sin ningún riesgo.
Ambas direcciones URL generadas realizan un recorrido de ida y vuelta con la definición de ruta siguiente (
/Home/Index y / ) y generan los mismos valores de ruta que se usaron para generar la dirección URL.

NOTE
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 información sobre la generación de direcciones URL, vea la sección Referencia de
generación de direcciones URL.

Uso de software intermedio de enrutamiento


Haga referencia al metapaquete Microsoft.AspNetCore.App 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();
}

Las rutas se deben configurar en el método Startup.Configure . En la aplicación de ejemplo se usan las API
siguientes:
RouteBuilder
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$)}/{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/ La solicitud pasa; no hay ninguna coincidencia.

GET /hello/Joe Hi, Joe!

POST /hello/Joe La solicitud pasa; solo coincide con HTTP GET.


IDENTIFICADOR URI RESPUESTA

GET /hello/Joe/Smith La solicitud pasa; no hay 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
(RequestDelegateRouteBuilderExtensions):
MapDelete
MapGet
MapMiddlewareDelete
MapMiddlewareGet
MapMiddlewarePost
MapMiddlewarePut
MapMiddlewareRoute
MapMiddlewareVerb
MapPost
MapPut
MapRoute
MapVerb
Algunos de los métodos enumerados, como MapGet, requieren un 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 coincida ( { o } ), repita el carácter ( {{ o }} ) para usarlo como secuencia de
escape.
Los patrones de dirección URL que intentan capturar un nombre de archivo con una extensión de archivo
opcional tienen consideraciones adicionales. Por ejemplo, considere la plantilla files/{filename}.{ext?} .
Cuando existen valores para filename y ext , los dos valores se rellenan. Si solo existe un valor para
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 un asterisco ( * ) o un asterisco doble ( ** ) como prefijo de un parámetro de ruta para
enlazar con el resto del URI. Se denominan parámetros 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.

El parámetro catch-all inserta los caracteres de escape correspondientes cuando se usa la ruta para generar
una dirección URL, incluidos caracteres de separación de ruta de acceso ( / ). Por ejemplo, la ruta
foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath . Tenga en cuenta la barra
diagonal de escape. Para los caracteres separadores de ruta de acceso de ida y vuelta, use el prefijo de
parámetro de ruta ** . La ruta foo/{**path} con { path = "my/path" } genera foo/my/path .
Se puede usar el asterisco ( * ) 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.
El parámetro catch-all inserta los caracteres de escape correspondientes cuando se usa la ruta para generar
una dirección URL, incluidos caracteres de separación de ruta de acceso ( / ). Por ejemplo, la ruta
foo/{*path} con valores de ruta { path = "my/path" } genera foo/my%2Fpath . Tenga en cuenta la barra
diagonal de escape.
Los parámetros de ruta pueden tener valores predeterminados designados mediante la especificación del
valor predeterminado 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. Los parámetros de ruta se pueden
convertir en opcionales si 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 lo proporciona.
Los parámetros de ruta 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 incluyen 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 y los argumentos se pasan 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.
Los parámetros de ruta también pueden tener transformadores de parámetros, que transforman el valor de
un parámetro al generar vínculos y acciones y páginas coincidentes para las direcciones URL. Al igual que
las restricciones, los transformadores de parámetros se pueden agregar en línea a un parámetro de ruta al
incorporar un carácter de dos puntos ( : ) y un nombre de transformador después del nombre del
parámetro de ruta. Por ejemplo, la plantilla de ruta blog/{article:slugify} especifica un transformador
slugify . Para obtener más información sobre los transformadores de parámetros, vea la sección Referencia
de transformadores de parámetros.
En la tabla siguiente se muestran algunas plantillas de ruta de ejemplo y su comportamiento.
PLANTILLA DE RUTA URI COINCIDENTE DE EJEMPLO EL URI DE LA SOLICITUD…

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 al


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 se ha producido una coincidencia con la dirección URL entrante
y la ruta de dirección URL se convierte en tokens 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.
Las restricciones se usan en las solicitudes de enrutamiento y la generación de vínculos.
WARNING
No use las restricciones para las validación de entrada. Si las restricciones se usan para la validación de entrada,
los resultados de entrada no válidos producirán un error 404 - No encontrado en lugar de un error 400 - Solicitud
incorrecta con un 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 de ejemplo y su comportamiento esperado.

RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

int {id:int} 123456789 , -123456789 Coincide con cualquier


entero

bool {active:bool} true , FALSE Coincide con true o


false (no distingue
mayúsculas de minúsculas)

datetime {dob:datetime} 2016-12-31 , Coincide con un valor


2016-12-31 7:32pm DateTime válido (en la
referencia cultural
invariable; vea la
advertencia)

decimal {price:decimal} 49.99 , -1,000.01 Coincide con un valor


decimal válido (en la
referencia cultural
invariable; vea la
advertencia)

double {weight:double} 1.234 , -1,001.01e8 Coincide con un valor


double válido (en la
referencia cultural
invariable; vea la
advertencia)

float {weight:float} 1.234 , -1,001.01e8 Coincide con un valor


float válido (en la
referencia cultural
invariable; vea la
advertencia)

guid {id:guid} CD2C1638-1638-72D5- Coincide con un valor


1638-DEADBEEF1638 Guid válido
,
{CD2C1638-1638-72D5-
1638-DEADBEEF1638}

long {ticks:long} 123456789 , -123456789 Coincide con un valor


long válido

minlength(value) {username:minlength(4)} Rick La cadena debe tener al


menos cuatro caracteres

maxlength(value) {filename:maxlength(8)} Richard La cadena no debe tener


más de ocho caracteres
RESTRICCIÓN EJEMPLO COINCIDENCIAS DE EJEMPLO NOTAS

length(length) {filename:length(12)} somefile.txt La cadena debe tener una


longitud de exactamente
12 caracteres

length(min,max) {filename:length(8,16)} somefile.txt La cadena debe tener una


longitud como mínimo de
ocho caracteres y como
máximo de 16

min(value) {age:min(18)} 19 El valor entero debe ser


como mínimo 18

max(value) {age:max(120)} 91 El valor entero debe ser


como máximo 120

range(min,max) {age:range(18,120)} 91 El valor entero debe ser


como mínimo 18 y máximo
120

alpha {name:alpha} Rick La cadena debe constar de


uno o más caracteres
alfabéticos ( a - z , no
distingue mayúsculas de
minúsculas)

regex(expression) {ssn:regex(^\\d{{3}}- 123-45-6789 La cadena debe coincidir


\\d{{2}}-\\d{{4}}$)} con la expresión regular
(vea las sugerencias sobre
cómo definir una expresión
regular)

required {name:required} Rick Se usa para exigir que un


valor que no es de
parámetro esté presente
durante la generación de
dirección URL

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, los caracteres \ (una barra diagonal inversa) de la cadena
se deben proporcionar como caracteres \\ (barra diagonal inversa doble) 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). Para aplicar secuencias de escape a los caracteres delimitadores de parámetro
de enrutamiento ( { , } , [ y ] ), duplique los caracteres en la expresión ( {{ , } , [[ y ]] ). En la tabla
siguiente se muestra una expresión regular y la versión con la secuencia de escape.

EXPRESIÓN REGULAR EXPRESIÓN REGULAR CON SECUENCIA DE ESCAPE

^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$

^[a-z]{2}$ ^[[a-z]]{{2}}$

Las expresiones regulares que se usan en el enrutamiento suelen empezar con el carácter de acento
circunflejo ( ^ ) y coinciden con la posición inicial de la cadena. Las expresiones suelen terminar con el
carácter del signo de dólar ( $ ) y coincidir con el 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 proporcionan ejemplos y se explica por qué 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

Para obtener más información sobre la sintaxis de expresiones regulares, vea Expresiones regulares de .NET
Framework.
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.

Restricciones de ruta personalizadas


Además de las restricciones de ruta integradas, se pueden crear restricciones de ruta personalizadas
implementando la interfaz IRouteConstraint. La interfaz IRouteConstraint contiene un único método, Match ,
que devuelve true si se cumple la restricción, y false en caso contrario.
Para utilizar una restricción IRouteConstraint personalizada, el tipo de restricción de ruta debe registrarse
con el parámetro ConstraintMap de la aplicación en el contenedor de servicios de la aplicación.
ConstraintMap es un diccionario que asigna claves de restricciones de ruta a implementaciones de
IRouteConstraint que validen esas restricciones. El parámetro ConstraintMap de una aplicación puede
actualizarse en Startup.ConfigureServices como parte de una llamada a services.AddRouting o
configurando RouteOptions directamente con services.Configure<RouteOptions> . Por ejemplo:

services.AddRouting(options =>
{
options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
});

La restricción puede aplicarse a las rutas de la manera habitual, usando el nombre especificado al registrar el
tipo de restricción. Por ejemplo:

[HttpGet("{id:customName}")]
public ActionResult<string> Get(string id)

Referencia de transformadores de parámetros


Transformadores de parámetros:
Se ejecutan al generar un vínculo para Route.
Implemente Microsoft.AspNetCore.Routing.IOutboundParameterTransformer .
Se configuran con ConstraintMap.
Toman el valor de ruta del parámetro y lo transforman en un nuevo valor de cadena.
Como resultado, el valor transformado se usa en el vínculo generado.
Por ejemplo, un transformador de parámetros personalizado slugify en el patrón de ruta
blog\{article:slugify} con Url.Action(new { article = "MyTestArticle" }) genera blog\my-test-article .

Para usar un transformador de parámetros en un patrón de ruta, configúrelo primero con ConstraintMap en
Startup.ConfigureServices :

services.AddRouting(options =>
{
// Replace the type and the name used to refer to it with your own
// IOutboundParameterTransformer implementation
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});

El marco usa los transformadores de parámetros para transformar el URI en el que se resuelve un punto de
conexión. Por ejemplo, ASP.NET Core MVC usa transformadores de parámetros para transformar el valor
de ruta usado para hacer coincidir elementos area , controller , action y page .

routes.MapRoute(
name: "default",
template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
Con la ruta anterior, la acción coincide con el URI
SubscriptionManagementController.GetAll()
/subscription-management/get-all . Un transformador de parámetros no cambia los valores de ruta usados
para generar un vínculo. Por ejemplo, Url.Action("GetAll", "SubscriptionManagement") genera
/subscription-management/get-all .

ASP.NET Core proporciona las convenciones de API para usar transformadores de parámetros con rutas
generadas:
ASP.NET Core MVC tiene la convención de API
Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention . Esta convención aplica un
transformador de parámetro especificado a todas las rutas de atributo de la aplicación. El transformador
de parámetro transforma los tokens de rutas de atributo a medida que se reemplazan. Para más
información, consulte Usar un transformador de parámetro para personalizar el reemplazo de tokens.
Razor Pages tiene la convención de API
Microsoft.AspNetCore.Mvc.ApplicationModels.PageRouteTransformerConvention . Esta convención aplica un
transformador de parámetros especificado a todas las instancias de Razor Pages detectadas de forma
automática. El transformador de parámetros transforma los segmentos de nombre de archivo y carpeta
de las rutas de Razor Pages. Para más información, consulte el artículo sobre cómo usar un
transformador de parámetro para personalizar rutas de Razor Pages.

Referencia de generación de direcciones URL


En el ejemplo siguiente se muestra cómo se genera un vínculo a una ruta, dado un diccionario de valores de
ruta y un valor RouteCollection.

app.Run(async (context) =>


{
var dictionary = new RouteValueDictionary
{
{ "operation", "create" },
{ "id", 123}
};

var vpc = new VirtualPathContext(context, null, dictionary,


"Track Package Route");
var path = routes.GetVirtualPath(vpc).VirtualPath;

context.Response.ContentType = "text/html";
await context.Response.WriteAsync("Menu<hr/>");
await context.Response.WriteAsync(
$"<a href='{path}'>Create Package 123</a><br/>");
});

El valor VirtualPath generado al final del ejemplo anterior es /package/create/123 . El 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 son adecuados porque limitan el número de valores que el desarrollador debe
especificar dentro de un contexto de solicitud. Los valores de ruta actuales de la solicitud actual se
consideran valores de ambiente para la generación de vínculos. En la acción About de HomeController de
una aplicación ASP.NET Core MVC, 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 que no coincidan con un parámetro se omiten. También se omiten los valores de
ambiente cuando un valor proporcionado de forma explícita invalida el valor de ambiente. La coincidencia se
produce de izquierda a derecha en la dirección URL.
Los valores que se proporcionan de forma explícita pero que no coinciden con un segmento de la ruta se
agregan a la cadena de consulta. En la tabla siguiente se muestra el resultado cuando se usa la plantilla de
ruta {controller}/{action}/{id?} .

VALORES DE AMBIENTE VALORES EXPLÍCITOS RESULTADO

controller = "Home" action = "About" /Home/About

controller = "Home" controller = "Order", action = /Order/About


"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 controller y action .

Segmentos complejos
Los segmentos complejos (por ejemplo, [Route("/x{token}y")] ), se procesan buscando coincidencias de
literales de derecha a izquierda de un modo no expansivo. Consulte este código para obtener una explicación
detallada de cómo se comparan los segmentos complejos. El código de ejemplo no se usa en ASP.NET Core,
pero proporciona una buena explicación de los segmentos complejos.
Controlar errores en ASP.NET Core
11/06/2019 • 17 minutes to read • Edit Online

Tom Dykstra, Luke Latham y Steve Smith


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). El artículo incluye instrucciones sobre cómo
establecer directivas de preprocesador ( #if , #endif y #define ) en la aplicación de ejemplo para habilitar
escenarios diferentes.

Página de excepciones para el desarrollador


En la Página de excepciones para el desarrollador se muestra información detallada sobre las excepciones de la
solicitud. La página está disponible mediante el paquete Microsoft.AspNetCore.Diagnostics, que se encuentra
en el metapaquete Microsoft.AspNetCore.App. Agregue código al método Startup.Configure para habilitar la
página cuando la aplicación se ejecuta en el entorno de desarrollo:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

Realice una llamada a UseDeveloperExceptionPage antes de cualquier middleware donde quiera capturar
excepciones.

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. Para más información sobre la configuración de entornos, consulte Usar varios entornos en
ASP.NET Core.

La página incluye la siguiente información sobre la excepción y la solicitud:


Seguimiento de la pila
Parámetros de cadena de consulta (si existen)
Cookies (si existen)
Encabezados
Para ver la Página de excepciones para el desarrollador en la aplicación de ejemplo, use la directiva de
preprocesador DevEnvironment y seleccione Trigger an exception (Desencadenar una excepción) en la página
principal.

Página del controlador de excepciones


Para configurar una página de control de errores personalizada para el entorno de producción, use el
middleware de control de excepciones. El middleware:
Captura y registra las excepciones.
Vuelve a ejecutar la solicitud en una canalización alternativa correspondiente a la página o el controlador
indicados. La solicitud no se vuelve a ejecutar si se ha iniciado la respuesta.
En el ejemplo siguiente, UseExceptionHandler agrega middleware de control de excepciones en entornos que
no son de desarrollo:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

La plantilla de aplicación de Razor Pages proporciona una página de error ( .cshtml) y una clase PageModel (
ErrorModel ) en la carpeta Pages. Para una aplicación de MVC, la plantilla de proyecto incluye un método de
acción para el error y una vista del error. Este es el método de acción:

[AllowAnonymous]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

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.
Acceso a la excepción
Use IExceptionHandlerPathFeature para acceder a la ruta de acceso a la solicitud original y a la excepción en una
página o un controlador de errores:

var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "File error thrown";
}
if (exceptionHandlerPathFeature?.Path == "/index")
{
ExceptionMessage += " from home page";
}

WARNING
No proporcione información de errores confidencial a los clientes. Proporcionar información de los errores es un riesgo
para la seguridad.

Para ver la página de control de excepciones en la aplicación de ejemplo, use las directivas de preprocesador
ProdEnvironment y ErrorHandlerPage y, después, seleccione Trigger an exception ( Desencadenar una
excepción) en la página principal.

Lambda del controlador de excepciones


Una alternativa a una página del controlador de excepciones personalizada es proporcionar una expresión
lambda en UseExceptionHandler. Usar una expresión lambda permite acceder al error antes de devolver la
respuesta.
Este es un ejemplo del uso de una expresión lambda para el control de excepciones:

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/html";

await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");


await context.Response.WriteAsync("ERROR!<br><br>\r\n");

var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();

// Use exceptionHandlerPathFeature to process the exception (for example,


// logging), but do NOT expose sensitive error information directly to
// the client.

if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
}

await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");


await context.Response.WriteAsync("</body></html>\r\n");
await context.Response.WriteAsync(new string(' ', 512)); // IE padding
});
});
app.UseHsts();
}

WARNING
No proporcione información de errores confidencial de IExceptionHandlerFeature o IExceptionHandlerPathFeature a los
clientes. Proporcionar información de los errores es un riesgo para la seguridad.

Para ver el resultado de la expresión lambda de control de excepciones en la aplicación de ejemplo, use las
directivas de preprocesador ProdEnvironment y ErrorHandlerLambda y, después, seleccione Trigger an
exception (Desencadenar una excepción) en la página principal.

UseStatusCodePages
Una aplicación ASP.NET Core no proporciona de forma predeterminada una página de códigos de estado para
los códigos de estado HTTP, como 404 - No encontrado. La aplicación devuelve un código de estado y un
cuerpo de respuesta vacío. Para proporcionar páginas de códigos de estado, use el middleware de páginas de
códigos de estado.
El middleware está disponible mediante el paquete Microsoft.AspNetCore.Diagnostics, que se encuentra en el
metapaquete Microsoft.AspNetCore.App.
Para habilitar los controladores de solo texto predeterminados para los códigos de estado de error comunes,
llame a UseStatusCodePages en el método Startup.Configure :

app.UseStatusCodePages();

Llame a UseStatusCodePages antes del middleware de control de solicitudes (por ejemplo, middleware de
archivos estáticos y middleware de MVC ).
Este es un ejemplo del texto que muestran los controladores predeterminados:

Status Code: 404; Not Found

Para ver alguno de los distintos formatos de la página de códigos de estado en la aplicación de ejemplo, use una
de las directivas de preprocesador que comienzan por StatusCodePages y seleccione Trigger a 404
(Desencadenar un error 404) en la página principal.

UseStatusCodePages con cadena de formato


Para personalizar el texto y el tipo de contenido de la respuesta, use la sobrecarga de UseStatusCodePages que
adopta una cadena de tipo de contenido y formato:

app.UseStatusCodePages(
"text/plain", "Status code page, status code: {0}");

UseStatusCodePages con una expresión lambda


Para especificar el código de escritura de respuesta y control de errores personalizado, use la sobrecarga de
UseStatusCodePages que adopta una expresión lambda:

app.UseStatusCodePages(async context =>


{
context.HttpContext.Response.ContentType = "text/plain";

await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});

UseStatusCodePagesWithRedirect
Método de extensión UseStatusCodePagesWithRedirects:
Envía un código de estado 302 - Encontrado al cliente.
Redirige al cliente a la ubicación proporcionada en la plantilla de dirección URL.

app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");
La plantilla de dirección URL puede incluir un marcador de posición {0} para el código de estado, como se
muestra en el ejemplo. Si la plantilla de dirección URL comienza con una tilde (~), esta se sustituye por el
atributo PathBase de la aplicación. Si 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. Para ver un ejemplo de Razor Pages, consulte
StatusCode.cshtml en la aplicación de ejemplo.
Este método se usa normalmente cuando la aplicación:
Debe redirigir al cliente a un punto de conexión diferente, normalmente en casos en los que una aplicación
diferente procesa el error. En el caso de aplicaciones web, la barra de direcciones del explorador del cliente
refleja el punto de conexión redirigido.
No debe conservar ni devolver el código de estado original con la respuesta de redirección inicial.

UseStatusCodePagesWithReExecute
Método de extensión UseStatusCodePagesWithReExecute:
Devuelve el código de estado original al cliente.
Genera el cuerpo de respuesta, para lo cual vuelve a ejecutar la canalización de solicitud mediante una ruta
de acceso alternativa.

app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");

Si 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. Para ver un ejemplo de Razor Pages, consulte StatusCode.cshtml en la aplicación de
ejemplo.
Este método se usa normalmente cuando la aplicación debe:
Procesar la solicitud sin redirigirla a un punto de conexión diferente. En el caso de aplicaciones web, la barra
de direcciones del explorador del cliente refleja el punto de conexión solicitado originalmente.
Conservar y devolver el código de estado original con la respuesta.
Las plantillas de dirección URL y cadena de consulta pueden incluir un marcador de posición ( {0} ) relativo al
código de estado. La plantilla de dirección URL debe empezar con una barra diagonal ( / ). Cuando se use un
marcador de posición en la ruta de acceso, confirme que el punto de conexión (página o controlador) puede
procesar el segmento de línea. Por ejemplo, una página de errores de Razor debe aceptar el valor del segmento
de línea opcional con la directiva @page :

@page "{code?}"

El punto de conexión que procesa el error puede obtener la dirección URL original que generó el error, como se
muestra en el ejemplo siguiente:

var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();


if (statusCodeReExecuteFeature != null)
{
OriginalURL =
statusCodeReExecuteFeature.OriginalPathBase
+ statusCodeReExecuteFeature.OriginalPath
+ statusCodeReExecuteFeature.OriginalQueryString;
}

Deshabilitar las páginas de códigos de estado


Para deshabilitar las páginas de códigos de estado de un método de acción o controlador MVC, use el atributo
[SkipStatusCodePages].
Para deshabilitar las páginas de códigos de estado en solicitudes específicas en un método de controlador de
Razor Pages o en un controlador MVC, use IStatusCodePagesFeature:

var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}

Código de control de excepciones


El código de las páginas de control de excepciones puede producir excepciones. Es recomendable que las
páginas de errores de producción incluyan únicamente contenido estático.
Encabezados de respuesta
Una vez enviados los encabezados de una respuesta:
La aplicación no puede cambiar el código de estado de la respuesta.
No se pueden ejecutar páginas o controladores de excepciones. 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, la implementación del servidor HTTP puede
controlar algunas excepciones. Si el servidor almacena en caché una excepción antes de que se envíen los
encabezados de respuesta, envía una respuesta 500 - Error interno del servidor sin cuerpo. Si el servidor
almacena en caché una excepción después de que se envían los encabezados de respuesta, cierra la conexión. El
servidor controla las solicitudes que no controla la aplicación. El control de excepciones del servidor controla
cualquier excepción que se produzca cuando el servidor controle la solicitud. Las páginas de error
personalizadas, el middleware de control de excepciones y los filtros de la aplicación no afectan 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. El
host puede configurarse para capturar errores de inicio y capturar errores detallados.
La capa de hospedaje puede mostrar una página de error para un error de inicio capturado solo si este se
produce después del enlace de puerto/dirección del host. Si se produce un error de enlace:
La capa de hospedaje registra una excepción crítica.
El proceso de dotnet se bloquea.
No se muestra ninguna página de error si el servidor HTTP es Kestrel.
Si se ejecuta en IIS o IIS Express, el módulo ASP.NET Core devuelve un error de proceso 502.5 si el proceso no
se puede iniciar. Para obtener más información, 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.

Página de error de la base de datos


El middleware de la página de error de la base de datos captura excepciones relacionadas con la base de datos
que se pueden resolver mediante migraciones de Entity Framework. Cuando se producen estas excepciones, se
genera una respuesta HTML con los detalles de las acciones posibles para resolver el problema. Esta página
debe habilitarse solo en el entorno de desarrollo. Habilitar la página mediante la adición de código a
Startup.Configure :

if (env.IsDevelopment())
{
app.UseDatabaseErrorPage();
}

Filtros de excepciones
En las aplicaciones de MVC, los filtros de excepciones se pueden configurar globalmente, o bien por controlador
o por acción. En las aplicaciones de Razor Pages, se pueden configurar a nivel global o por modelo de página.
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. Para obtener más información, vea Filtros en ASP.NET Core.

TIP
Los filtros de excepciones son útiles para interceptar las excepciones que se producen en las acciones de MVC, pero no
son tan flexibles como el middleware de control de excepciones. Se recomienda usar el middleware. Use filtros únicamente
cuando deba realizar el control de errores de manera diferente según la acción de MVC elegida.

Errores de estado del modelo


Para obtener información sobre cómo controlar los errores de estado de los modelos, vea Enlace de modelos y
Validación de modelos.

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
Realización de solicitudes HTTP mediante
IHttpClientFactory en ASP.NET Core
20/05/2019 • 25 minutes to read • Edit Online

Por Glenn Condron, Ryan Nowak y Steve Gordon


Se puede registrar y usar una interfaz 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 una interfaz IHttpClientFactory en cualquier parte donde se puedan
insertar servicios por medio de la inserción de dependencias (DI). 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/AspNetCore.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/AspNetCore.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/AspNetCore.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. IHttpClientFactory


crea un ámbito de inserción de dependencias independiente para cada controlador. Los controladores pueden
depender de servicios de cualquier ámbito. Los servicios de los que dependen los controladores se eliminan
cuando se elimina el controlador.
Una vez registrado, se puede llamar a AddHttpMessageHandler, pasando el tipo del controlador.
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 servicio transitorio, nunca como servicio con ámbito. Si el
controlador está registrado como un servicio con ámbito y los servicios de los que depende el controlador son
descartables, los servicios del controlador podrían eliminarse antes de que el controlador salga del ámbito, lo que
haría que se produjeran errores en el controlador.
Una vez registrado, se puede llamar a AddHttpMessageHandler, con lo que se pasa el tipo de 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>();

Use uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:
Pase datos al controlador usando HttpRequestMessage.Properties .
Use IHttpContextAccessor para acceder a la solicitud actual.
Cree un objeto de almacenamiento AsyncLocal personalizado para pasar los datos.

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. Las extensiones de Polly:

permiten agregar controladores basados en Polly a los clientes;


se pueden usar tras instalar el paquete NuGet Microsoft.Extensions.Http.Polly, aunque este no está incluido en
la plataforma compartida ASP.NET Core.
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 GET saliente es del tipo HTTP, 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 funcionalidad mejorada:

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 .
Hay un controlador HttpMessageHandler por cliente con nombre. La fábrica administra la duración de las
instancias de HttpMessageHandler .
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 el 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. Se puede usar el método
de extensión ConfigurePrimaryHttpMessageHandler para definir un delegado. Este delegado servirá para crear y
configurar el elemento principal HttpMessageHandler que ese cliente usa:

services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});

Recursos adicionales
Uso de HttpClientFactory para implementar solicitudes HTTP resistentes
Implementación de reintentos de llamada HTTP con retroceso exponencial con HttpClientFactory y las
directivas de Polly
Implementación del patrón de interruptor
Archivos estáticos en ASP.NET Core
10/05/2019 • 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.
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>();
}

Establezca la raíz de contenido en el directorio actual invocando a UseContentRoot dentro de Program.Main :

public class Program


{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();

host.Run();
}
}

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.
Si el destino es .NET Framework, agregue el paquete Microsoft.AspNetCore.StaticFiles al proyecto. Si el
destino es .NET Core, el metapaquete Microsoft.AspNetCore.App incluye este paquete.
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.
Agregue el paquete Microsoft.AspNetCore.StaticFiles al proyecto.
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" />

En el código anterior, el carácter de tilde de la ñ ~/ apunta a la raíz web. Para obtener más información, vea
Raíz web.
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 al que el middleware de archivos estáticos tenga
acceso.
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.s MyStaticFiles/images/banner1.svg
vg

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 realizan un redireccionamiento del lado cliente desde
http://{SERVER ADDRESS}/StaticFiles (sin una barra diagonal final) hasta
http://{SERVER ADDRESS}/StaticFiles/ (con una barra diagonal final). Las direcciones URL relativas dentro del
directorio StaticFiles no son 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 con un tipo de archivo desconocido, el middleware de archivos estáticos pasa la solicitud al
siguiente middleware de la canalización. Si ningún middleware se ocupa de la solicitud, se devuelve una
respuesta 404 No encontrado. Si se habilita la exploración de directorios, se muestra un vínculo al archivo en
una lista de directorios.
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
Introducción a las páginas de Razor en ASP.NET
Core
03/07/2019 • 35 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
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't
work with Visual Studio.

Creación de un proyecto de Razor Pages


Visual Studio
Visual Studio para Mac
Visual Studio Code
Vea Introducción a Razor Pages para obtener instrucciones detalladas sobre cómo crear un proyecto Razor
Pages.

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 se parece mucho a un archivo de vista de Razor que se utiliza en una aplicación ASP.NET
Core con controladores y vistas. 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.

Escritura de 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, los asistentes de etiquetas y los
asistentes 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 del
asistente 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 del asistente 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
predeterminado que se devuelve 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.

WARNING
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 el enlace de
GET , 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>

El asistente 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 . Use el atributo asp-area para especificar un área. Para más información,
consulte Áreas de ASP.NET Core.
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 ).

Marcado de las propiedades de página según sea necesario


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 obtener más información, vea Validación de modelos.

Administración de solicitudes HEAD con el controlador OnGet


Las solicitudes HEAD le permiten recuperar los encabezados de un recurso específico. A diferencia de las
solicitudes GET, las solicitudes HEAD no devuelven un cuerpo de respuesta.
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. En ASP.NET Core 2.1 y 2.2, este
comportamiento se produce con SetCompatibilityVersion en Startup.Configure :

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

Las plantillas predeterminadas generan la llamada SetCompatibilityVersion en ASP.NET Core 2.1 y 2.2.
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 asistentes 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, asistentes 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 los asistentes 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.
Para más información sobre las vistas parciales, vea Vistas parciales en ASP.NET Core.

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).
Para redirigir a una página en otra área, especifique el área:

RedirectToPage("/Index", new { area = "Services" });

Para más información, consulte Áreas de ASP.NET Core.

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

Para obtener más información, vea TempData.

Varios controladores por página


La siguiente página genera marcado para dos controladores de páginas mediante el asistente 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");

Recursos adicionales
Introducción a ASP.NET Core
Referencia de sintaxis de Razor para ASP.NET Core
Áreas de ASP.NET Core
Tutorial: Introducción a Razor Pages en ASP.NET Core
Convenciones de autorización de las páginas de Razor en ASP.NET Core
Convenciones de aplicación y de ruta de páginas de Razor en ASP.NET Core
Pruebas unitarias páginas de Razor en ASP.NET Core
Vistas parciales en ASP.NET Core
Tutorial: Creación de una aplicación web de páginas
de Razor con ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

En esta serie de tutoriales se explican los conceptos básicos de creación de una aplicación web de Razor Pages.
Para acceder a una introducción más avanzada pensada para desarrolladores con experiencia, consulte
Introducción a Razor Pages.
Esta serie incluye los siguientes tutoriales:
1. Creación de una aplicación web de 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 una base de datos
5. Actualización de páginas
6. Agregar búsqueda
7. Agregar un campo nuevo
8. Agregar validación
Al final, conseguirá una aplicación que puede mostrar y administrar una base de datos de películas.

Recursos adicionales
Versión en YouTube de este tutorial
Tutorial: Introducción a Razor Pages en ASP.NET
Core
04/07/2019 • 11 minutes to read • Edit Online

Por Rick Anderson


Este es el primer tutorial de una serie. En la serie se enseñan los conceptos básicos de la compilación de una
aplicación web de Razor Pages en ASP.NET Core.
Para acceder a una introducción más avanzada pensada para desarrolladores con experiencia, consulte
Introducción a Razor Pages.
Al final de la serie, tendrá una aplicación que puede administrar una base de datos de películas.
Vea o descargue el código de ejemplo (cómo descargarlo).
En este tutorial ha:
Crear una aplicación web de Razor Pages.
Ejecutar la aplicación.
Examinar los archivos de proyecto.
Al final de este tutorial, tendrá una aplicación web de Razor Pages que compilará en los tutoriales
posteriores.

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later
WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't
work with Visual Studio.

Creación de una aplicación web de páginas de Razor


Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú Archivo de Visual Studio, seleccione Nuevo > Proyecto.
Cree una nueva aplicación web de ASP.NET Core y seleccione Siguiente.

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.2 en la lista desplegable, después Aplicación web y, por último, Crear.

Se crea el proyecto de inicio siguiente:


Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:

Haga clic en Sí si confía en el certificado SSL de IIS Express.


Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener más información, vea Confiar en el certificado de desarrollo de ASP.NET Core HTTPS .
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 página principal de la aplicación, seleccione Aceptar para dar su consentimiento al
seguimiento.
Esta aplicación no realiza un seguimiento de la información personal, pero la plantilla del proyecto
incluye la función de consentimiento en caso de que sea necesaria para cumplir con el Reglamento
general de protección de datos (RGPD ) de la Unión Europea.

En la siguiente imagen se muestra la aplicación tras haber dado su consentimiento al seguimiento:


Examen de los archivo del proyecto
He aquí un resumen de las principales carpetas y archivos del proyecto con los que va a trabajar en los
próximos tutoriales.
Carpeta Pages
Contiene Razor Pages y los archivos auxiliares. Cada página de Razor se compone de un par de archivos:
Archivo .cshtml que contiene el marcado HTML con código C# que usa la sintaxis Razor.
Archivo . cshtml.cs que contiene C# código que controla los eventos de página.
Los archivos auxiliares tienen nombres que comienzan con un carácter de subrayado. Por ejemplo, el
archivo _Layout.cshtml configura los elementos de la interfaz de usuario comunes a todas las páginas. Este
archivo configura el menú de navegación de la parte superior de la página y el aviso de copyright de la
parte inferior de la página. Para más información, consulte Diseño en ASP.NET Core.
Carpeta wwwroot
Contiene los archivos estáticos, como los archivos HTML, los archivos de JavaScript y los archivos CSS.
Para más información, consulte Archivos estáticos en ASP.NET Core.
appSettings.json
Contiene los datos de configuración, como las cadenas de conexión. Para más información, consulte
Configuración en ASP.NET Core.
Program.cs
Contiene el punto de entrada del programa. Para más información, consulte Host genérico de .NET.
Startup.cs
Contiene código que configura el comportamiento de la aplicación, como, por ejemplo, si se requiere
consentimiento para las cookies. Para más información, consulte Inicio de la aplicación en ASP.NET Core.

Recursos adicionales
Versión en YouTube de este tutorial

Pasos siguientes
En este tutorial ha:
Creado una aplicación web de Razor Pages.
Ejecutado la aplicación.
Examinado los archivo del proyecto.
Pase al siguiente tutorial de la serie:

A GREGA R UN
M ODELO
Agregar un modelo a una aplicación de páginas de
Razor en ASP.NET Core
10/05/2019 • 20 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En esta sección, se agregan 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.
Las clases de modelo 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.
Vea o descargue un ejemplo.

Agregar un modelo de datos


Visual Studio
Visual Studio Code
Visual Studio para Mac
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
Película.
Agregue las propiedades siguientes a la clase Movie :

using System;
using System.ComponentModel.DataAnnotations;

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

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

la clase Movie contiene:


La base de datos requiere el campo ID para la clave principal.
[DataType(DataType.Date)] : El atributo DataType especifica el tipo de datos (Date). Con este atributo:
El usuario no tiene que especificar información horaria en el campo de fecha.
Solo se muestra la fecha, no información horaria.
Los elementos DataAnnotations se tratan en un tutorial posterior.
Compile el proyecto para comprobar que no haya errores de compilación.

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
Cree una carpeta Pages/Movies:
Haga clic con el botón derecho en la carpeta Páginas > Agregar > Nueva carpeta.
Asigne a la carpeta el nombre Movies.
Haga clic con el botón derecho en la carpeta Pages/Movies > Agregar > Nuevo elemento con scaffolding.

En el cuadro de diálogo Agregar scaffold, seleccione Páginas de Razor 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.
Seleccione Agregar.

El archivo appsettings.json se actualiza con la cadena de conexión que se usa para conectarse a una base de datos
local.
El proceso de scaffolding crea y actualiza los archivos siguientes:
Archivos creados
Pages/Movies: Create, Delete, Details, Edit e Index.
Data/RazorPagesMovieContext.cs
Archivo actualizado
Startup.cs
Los archivos creados y actualizados se explican en la sección siguiente.
Migración inicial
Visual Studio
Visual Studio Code
Visual Studio para Mac
En esta sección, la Consola del administrador de paquetes (PMC ) se utiliza 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

Los comandos anteriores generan la advertencia siguiente: "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
'HasColumnType()'." ("No se ha especificado ningún tipo en la columna decimal 'Price' en el tipo de entidad
'Movie'. Esto hará que los valores se trunquen inadvertidamente si no caben según la precisión y escala
predeterminados. Especifique expresamente el tipo de columna de SQL Server que tenga cabida para todos los
valores usando 'HasColumnType()'.")
Puede omitir dicha advertencia, ya que se corregirá en un tutorial posterior.
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 RazorPagesMovieContext.cs. El
argumento InitialCreate se usa para asignar nombre a las migraciones. Se puede usar cualquier nombre, pero,
por convención, se selecciona uno que describa la migración.
El comando ef database update ejecuta el método Up en el archivo Migrations/<time-stamp>_InitialCreate.cs.
El método Up crea la base de datos.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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:

// 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_2);

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

El elemento RazorPagesMovieContext coordina la funcionalidad de EF Core (creación, lectura, actualización,


eliminación, etc.) para el modelo Movie . El contexto de datos ( RazorPagesMovieContext ) se deriva de
Microsoft.EntityFrameworkCore.DbContext. En el contexto de datos se especifica qué entidades se incluyen en el
modelo de datos.

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


}
}

El código anterior 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.
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. Se puede usar cualquier nombre, pero, por
convención, se utiliza uno que describa la migración. Para obtener más información, vea Tutorial: Uso de la
característica de migraciones: ASP.NET MVC con EF Core.
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.
Prueba de la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).

Si se produce un error:

SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.

Quiere decir que falta el paso de migraciones.


Pruebe el vínculo Crear.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . La aplicación debe globalizarse 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. Para obtener instrucciones sobre la
globalización, consulte esta cuestión en GitHub.

Pruebe los vínculos Editar, Detalles y Eliminar.


En el tutorial siguiente se explican los archivos creados mediante scaffolding.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : S IG U IE N T E : R A Z O R P A G E S C O N
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
10/05/2019 • 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 de creación, eliminación, detalles y edición


Examine el modelo de página Pages/Movies/Index.cshtml.cs:

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.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 RazorPagesMovieContext 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";
}

<h1>Index</h1>

<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.DisplayFor de la página.
Página de diseño
Seleccione los vínculos de menú (RazorPagesMovie [Película de Razor Pages], Home [Inicio] y Privacy
[Privacidad]). Cada página muestra el mismo diseño de menú. El diseño de menú se implementa en el archivo
Pages/Shared/_Layout.cshtml. Abra el archivo Pages/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 vistas específicas de página que cree, encapsuladas en la página de
diseño. Por ejemplo, si selecciona el vínculo Privacy (Privacidad), la vista Pages/Privacy.cshtml se
representará dentro del método RenderBody .
Propiedades ViewData y Layout
Tenga en cuenta el siguiente código del archivo Pages/Movies/Index.cshtml:

@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 _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 que no aparece en el archivo de diseño. A
diferencia de los comentarios HTML ( <!-- --> ), los comentarios de Razor no se envían al cliente.
Actualizar el diseño
Cambie el elemento <title> del archivo Pages/Shared/_Layout.cshtml para mostrar Movie en lugar de
RazorPagesMovie.

<!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/Shared/_Layout.cshtml.

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

Reemplace el elemento anterior por el marcado siguiente.

<a class="navbar-brand" asp-page="/Movies/Index">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 . El valor de atributo asp-area está vacío, por lo que no se usa el área del vínculo. Consulte
Áreas para obtener más información.
Guarde los cambios y pruebe la aplicación haciendo clic en el vínculo RpMovie. Si tiene cualquier problema,
consulte el archivo _Layout.cshtml en GitHub.
Pruebe los otros vínculos (Inicio, RpMovie, 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.

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.
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;
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, hablaremos 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";
}

<h1>Create</h1>

<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-primary" />
</div>
</form>
</div>
</div>

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

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

Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio muestra la etiqueta <form method="post"> con una fuente negrita diferenciada que se aplica a los
asistentes de etiquetas:
El elemento <form method="post"> es un asistente de etiquetas de formulario. El asistente 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>

Los asistentes 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.
El asistente 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 .
El asistente 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.

Recursos adicionales
Versión en YouTube de este tutorial
A N T E R IO R : A D IC IÓ N D E U N S IG U IE N T E : B A S E D E
M ODELO D A TOS
Trabajar con una base de datos y ASP.NET Core
10/05/2019 • 14 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


Vea o descargue el código de ejemplo (cómo descargarlo).
El objeto RazorPagesMovieContext 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 de Startup.cs:
Visual Studio
Visual Studio Code
Visual Studio para Mac

// 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_2);

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-
1234;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Cuando la aplicación se implementa en un servidor de prueba o producción, se puede utilizar una variable de
entorno para establecer la cadena de conexión en un servidor de base de datos real. Para más información, vea
Configuración.
Visual Studio
Visual Studio Code
Visual Studio para Mac

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

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:
Inicializar la base de datos
Cree una nueva clase denominada SeedData en la carpeta Models con el código 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 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.

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
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : R A Z O R P A G E S C O N S IG U IE N T E : A C T U A L IZ A C IÓ N D E
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
10/05/2019 • 9 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas con scaffolding pinta bien, pero la presentación no es ideal. FechaDeLanzamiento
debe ser Fecha de lanzamiento (tres 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;
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; }
}
}

La anotación de datos [Column(TypeName = "decimal(18, 2)")] permite que Entity Framework Core asigne
correctamente Price a la moneda en la base de datos. Para más información, vea Tipos de datos.
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, el ?id=1 de https://localhost:5001/Movies/Details?id=1 ).
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?}"
Para probar el comportamiento de @page "{id:int?}" :
Establezca la directiva de página de Pages/Movies/Details.cshtml en @page "{id:int?}" .
Establezca un punto de interrupción en public async Task<IActionResult> OnGetAsync(int? id) (en
Pages/Movies/Details.cshtml.cs).
Navegue a https://localhost:5001/Movies/Details/ .

Con la directiva @page "{id:int}" , el punto de interrupción nunca se alcanza. El motor de enrutamiento
devuelve HTTP 404. Con @page "{id:int?}" , el método OnGetAsync devuelve NotFound (HTTP 404).
Aunque no se recomienda, puede escribir el método OnGetAsync (en Pages/Movies/Delete.cshtml.cs) como:

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


{
if (id == null)
{
Movie = await _context.Movie.FirstOrDefaultAsync();
}
else
{
Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
}

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

Pruebe el código anterior:


Seleccione un vínculo Eliminar.
Quite el identificador de la dirección URL. Por ejemplo, cambie https://localhost:5001/Movies/Delete/8 a
https://localhost:5001/Movies/Delete .
Ejecute paso a paso el código del depurador.
Revisión del control de excepciones de simultaneidad
Revise el método OnPostAsync en el archivo Pages/Movies/Edit.cshtml.cs:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}

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

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

return RedirectToPage("./Index");
}

private bool MovieExists(int id)


{
return _context.Movie.Any(e => e.ID == id);
}

El código anterior detecta las excepciones de simultaneidad cuando el cliente uno elimina la película y el otro
cliente publica cambios en ella.
Para probar el bloque catch :
Establecer un punto de interrupción en catch (DbUpdateConcurrencyException)
Seleccione Editar para una película y realice cambios, pero no seleccione Guardar.
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.
Es posible que el código de producción quiera detectar conflictos de simultaneidad. 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 RazorPagesMovieContext _context;

public EditModel(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 muestra 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.

Recursos adicionales
Versión en YouTube de este tutorial

A N T E R IO R : T R A B A J O C O N U N A B A S E D E S IG U IE N T E : A D IC IÓ N D E
D A TOS BÚSQUEDA
Agregar búsqueda a páginas de Razor de ASP.NET
Core
10/05/2019 • 8 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En las secciones siguientes, se ha agregado la función de buscar películas por género o nombre.
Agregue las siguientes propiedades resaltadas a Pages/Movies/Index.cshtml.cs:

public class IndexModel : PageModel


{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)


{
_context = context;
}

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


[BindProperty(SupportsGet = true)]
public string SearchString { get; set; }
// Requires using Microsoft.AspNetCore.Mvc.Rendering;
public SelectList Genres { get; set; }
[BindProperty(SupportsGet = true)]
public string MovieGenre { get; set; }

SearchString : contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda. El elemento
SearchString está decorado con el atributo [BindProperty] . [BindProperty] enlaza los valores del
formulario y las cadenas de consulta con el mismo nombre que la propiedad. (SupportsGet = true) se
necesita para el enlace de las solicitudes GET.
Genres : contiene la lista de géneros. Genres permite al usuario seleccionar un género de la lista. SelectList
requiere using Microsoft.AspNetCore.Mvc.Rendering; .
MovieGenre : contiene el género concreto que selecciona el usuario (por ejemplo, "Western").
Genres y MovieGenre se utilizan posteriormente en este tutorial.

WARNING
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 el enlace de GET , 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)]

Actualice el método OnGetAsync de la página de índice con el código siguiente:


public async Task OnGetAsync()
{
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:

// using System.Linq;
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 la propiedad SearchString no es NULL ni está vacía, 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, https://localhost:5001/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, https://localhost:5001/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.

El entorno de ejecución de ASP.NET Core usa el enlace de modelos para establecer el valor de la propiedad
SearchString de la cadena de consulta ( ?searchString=Ghost ) o de los datos de ruta (
https://localhost:5001/Movies/Ghost ). El enlace de modelos no hace distinción entre mayúsculas y minúsculas.

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

<h1>Index</h1>

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

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

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

La etiqueta HTML <form> usa los siguientes Asistentes de etiquetas:


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 a través de la cadena de consulta.
Asistente de etiquetas de entrada
Guarde los cambios y pruebe el filtro.
Búsqueda por género
Actualice el método OnGetAsync con el código siguiente:

public async Task OnGetAsync()


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

Agregar búsqueda por género a la página de Razor


Actualice Index.cshtml como se indica a continuación:

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h1>Index</h1>

<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" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>

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

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

Recursos adicionales
Versión en YouTube de este tutorial

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 : Adición de un nuevo campo


P Á G IN A S
Agregar un campo nuevo a una página de Razor en
ASP.NET Core
10/05/2019 • 10 minutes to read • Edit Online

Por Rick Anderson


Vea o descargue el código de ejemplo (cómo descargarlo).
En esta sección, Migraciones de Entity Framework Code First se utiliza para:
Agregar un campo nuevo al modelo.
Migrar el cambio de esquema del campo nuevo 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; }

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


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

Compile la aplicación.
Edite Pages/Movies/Index.cshtml y agregue un campo Rating :

@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h1>Index</h1>

<p>
<a asp-page="Create">Create New</a>
<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" asp-for="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>

Actualice las páginas siguientes:


Agregue el campo Rating a las páginas Delete y Details.
Actualice Create.cshtml con un campo Rating .
Agregue el campo Rating a la página de edición.

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

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

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

Vea el archivo completado SeedData.cs.


Compile la solución.
Visual Studio
Visual Studio Code/Visual Studio para Mac
Agregar una migración para el campo de clasificació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.
El comando Update-Database le indica al marco que aplique los cambios de esquema a la base de datos.
Si elimina todos los registros de la base de datos, el inicializador inicializará la base de datos e incluirá 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).
Otra opción es eliminar la base de datos y usar las migraciones para volver a crear la base de datos. Para
eliminar la base de datos de 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 se ha propagado, establezca un punto de interrupción en el método SeedData.Initialize .

Recursos adicionales
Versión en YouTube de este tutorial

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
BÚSQUEDA V A L ID A C IÓ N
Agregar la validación a una página de Razor de
ASP.NET Core
19/05/2019 • 14 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.
Hacer 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.

Add validation rules to the movie model


Open the Movie.cs file. The DataAnnotations namespace provides a set of built-in validation attributes that are
applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType
that help with formatting and don't provide any validation.
Update the Movie class to take advantage of the built-in Required , StringLength , RegularExpression , and
Range validation attributes.
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; }
}

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing
prevents a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not
allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for
a "Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also
ensures that you can't forget to validate something and inadvertently let bad data into the database.
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 decimales en campos decimales. 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 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. Puede hacerlo con las herramientas para desarrolladores del
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)]


[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
Para obtener información sobre la implementación en Azure, consulte Tutorial: Compilación de una aplicación
ASP.NET en Azure con SQL Database. Estas instrucciones son para una aplicación ASP.NET, no para una
aplicación ASP.NET Core, pero los pasos son los mismos.
Gracias por seguir esta introducción a las páginas de Razor. Introducción a MVC con Razor Pages y EF Core es
un excelente artículo de seguimiento de este tutorial.

Recursos adicionales
Asistentes de etiquetas en formularios de ASP.NET Core
Globalización y localización en ASP.NET Core
Asistentes de etiquetas en ASP.NET Core
Crear asistentes de etiquetas en ASP.NET Core
Versión en YouTube de este tutorial

A N T E R IO R : Adición de un nuevo campo


Métodos de filtrado de páginas de Razor en ASP.NET
Core
10/05/2019 • 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 después de que se ha seleccionado un método de controlador, pero
antes de modelo se produce el enlace.
OnPageHandlerExecuting : Se llama antes de que se ejecuta el método de controlador, una vez
completado el enlace de modelos.
OnPageHandlerExecuted : Llamado cuando se ejecuta el método de controlador, antes de resultado de la
acción.
Métodos asincrónicos:
OnPageHandlerSelectionAsync : Llamar de forma asincrónica una vez que se ha seleccionado el método
de controlador, pero antes de que se produce el enlace de modelos.
OnPageHandlerExecutionAsync : Se llama asincrónicamente antes de invoca el método de controlador,
una vez completado el enlace de modelos.
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();
}
}
Convenciones de aplicación y de ruta de páginas de
Razor en ASP.NET Core
10/05/2019 • 22 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
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)

Convenciones de páginas de Razor se agregan y configuran mediante el AddRazorPagesOptions método de


extensión para AddMvc en la colección de servicios en la Startup clase. 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).

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

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.
El enrutamiento del controlador de MVC y el enrutamiento de Razor Pages comparten 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
Agregar un delegado para IPageConvention para agregar convenciones del modelo que se aplican a las 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 IPageConvention
instancias que se aplican durante la ruta de página construcción del modelo.
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.
Opciones de páginas de Razor, como agregar Conventions, se agregan cuando MVC se agrega a la colección de
servicios 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 IPageConvention
instancias que se aplican 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 IPageConvention
instancias que se aplican durante la construcción del modelo de controlador de página.

public class GlobalPageHandlerModelConvention


: IPageHandlerModelConvention
{
public void Apply(PageHandlerModel model)
{
// Access the PageHandlerModel
}
}

Startup.cs:

options.Conventions.Add(new GlobalPageHandlerModelConvention());

Convenciones de acción de ruta de página


El proveedor de modelo de ruta predeterminado que se deriva de IPageRouteModelProvider invoca
convenciones que están diseñadas para proporcionar puntos de extensibilidad para configurar las rutas de la
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 PageRouteModel para todas las páginas 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 PageRouteModel para 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:
Usar un transformador de parámetro para personalizar las rutas de
página
Las rutas de página generadas por ASP.NET Core pueden personalizarse mediante un transformador de
parámetro. Un transformador de parámetro implementa IOutboundParameterTransformer y transforma el valor de
parámetros. Por ejemplo, un transformador de parámetros SlugifyParameterTransformer personalizado cambia el
valor de ruta SubscriptionManagement a subscription-management .
El PageRouteTransformerConvention convención de modelo de ruta de página aplica un transformador de
parámetro a los segmentos de nombre de carpeta y archivo de rutas de página generada automáticamente en
una aplicación. Por ejemplo, las páginas de Razor de archivos en
/Pages/SubscriptionManagement/ViewAll.cshtml tendría su ruta reescribe desde
/SubscriptionManagement/ViewAll a /subscription-management/view-all .

PageRouteTransformerConvention solo transforma los segmentos de una ruta de página generados


automáticamente que proceden de la carpeta de las páginas de Razor y el nombre de archivo. No transforma los
segmentos de ruta agregados con el @page directiva. La convención también no transforma las rutas agregadas
por AddPageRoute.
El PageRouteTransformerConvention está registrado como una opción en Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add(
new PageRouteTransformerConvention(
new SlugifyParameterTransformer()));
});
}

public class SlugifyParameterTransformer : IOutboundParameterTransformer


{
public string TransformOutbound(object value)
{
if (value == null) { return null; }

// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}

Configurar una ruta de página


Use AddPageRoute para configurar una ruta a una página en la ruta de acceso de la 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 modelo de página predeterminado que implementa IPageApplicationModelProvider invoca
convenciones que están diseñadas para proporcionar puntos de extensibilidad para configurar los modelos de
página. Estas convenciones son útiles al crear y modificar escenarios de detección y procesamiento de páginas.
Los ejemplos en esta sección, la aplicación de ejemplo usa un AddHeaderAttribute (clase), que es un
ResultFilterAttribute, que 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 PageApplicationModel instancias para todas las páginas 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 configura el filtro que se aplican. 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:

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. Otros tipos de filtros de MVC están disponibles para su uso: Autorización, excepción,
recursos, y resultado. 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 en ASP.NET Core
Áreas de ASP.NET Core
Cargar archivos en una página de Razor en ASP.NET
Core
10/05/2019 • 27 minutes to read • Edit Online

Por Luke Latham


En este tema se basa en el aplicación de ejemplo en Tutorial: Introducción a Razor Pages 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.
Vea o descargue el código de ejemplo (cómo descargarlo)

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 del asistente 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 del asistente estático. Cree una carpeta Utilities en la aplicación y agregue un archivo
FileHelpers.cs con el siguiente contenido. El método del asistente, 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.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using RazorPagesMovie.Models;

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

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


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

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

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

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


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

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

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


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

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


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

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


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

return string.Empty;
}
}
}

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

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


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

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


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

return string.Empty;
}
}
}

Guardar el archivo en el disco


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

public async Task<IActionResult> OnPostAsync()


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

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

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


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

return RedirectToPage("./Index");
}

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

NOTE
filePath debe incluir el nombre de archivo. Si no se ha proporcionado un nombre de archivo, se produce una excepción
UnauthorizedAccessException en tiempo de ejecución.
WARNING
Los archivos cargados nunca deben persistir en el mismo árbol de directorio que la aplicación.
En el código de ejemplo no se proporciona ningún tipo de protección de servidor frente a cargas de archivos
malintencionados. Vea los siguientes recursos para más información sobre cómo reducir el área expuesta de ataques al
aceptar archivos de los usuarios:
Unrestricted File Upload (Carga de archivos sin restricciones)
Seguridad de Azure: 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 de NuGet
> Package Manager Console.

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 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]
[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 obtener más información con IFormFile cargar, consulte cargas de archivos en ASP.NET Core: Solución de
problemas.
SDK de Razor de ASP.NET Core
18/06/2019 • 10 minutes to read • Edit Online

Por Rick Anderson

Información general
El SDK de .NET Core 2.1 o versiones posteriores incluye la Microsoft.NET.Sdk.Razor MSBuild SDK (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.
El SDK de Razor incluye un <Content> elemento con un Include atributo establecido en el **\*.cshtml patrón
global. Se publican los archivos coincidentes.
El SDK de Razor incluye <Content> elementos con Include atributos establecidos en el **\*.cshtml y
**\*.razor patrones de comodines. Se publican los archivos coincidentes.

Requisitos previos
SDK de .NET Core 2.1 o versiones posteriores

Usar el SDK de Razor


La mayoría de las aplicaciones web no están necesario hacer referencia explícitamente el 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">
<!-- omitted for brevity -->
</Project>

Normalmente, una referencia de paquete para Microsoft.AspNetCore.Mvc se requiere para recibir las
dependencias adicionales que son necesarios para generar y compilar las páginas de Razor y las vistas de
Razor. Como mínimo, el proyecto debe agregar referencias de paquete para:
Microsoft.AspNetCore.Razor.Design
Microsoft.AspNetCore.Mvc.Razor.Extensions
Microsoft.AspNetCore.Mvc.Razor
El Microsoft.AspNetCore.Razor.Design paquete proporciona las tareas de compilación de Razor y destinos
para el proyecto.
Los paquetes anteriores están incluidos en Microsoft.AspNetCore.Mvc . El marcado siguiente muestra un
archivo de proyecto que usa el SDK de Razor para generar 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.3" />
</ItemGroup>

</Project>

WARNING
El Microsoft.AspNetCore.Razor.Design y Microsoft.AspNetCore.Mvc.Razor.Extensions paquetes se incluyen en el
Microsoft.AspNetCore.App metapaquete. Sin embargo, la versión de menor Microsoft.AspNetCore.App referencia de
paquete proporciona un metapaquete a la aplicación que no incluye la versión más reciente de
Microsoft.AspNetCore.Razor.Design . Deben hacer referencia a una versión coherente de los proyectos
Microsoft.AspNetCore.Razor.Design (o Microsoft.AspNetCore.Mvc ) para que se incluyen las correcciones más
recientes de tiempo de compilación para Razor. Para obtener más información, consulte este problema de GitHub.

Propiedades
Las siguientes propiedades controlan el comportamiento del SDK de Razor como parte de la generación de un
proyecto:
RazorCompileOnBuild – Cuando true , compila y se emite el ensamblado de Razor como parte de la creación
del proyecto. Tiene como valor predeterminado true .
RazorCompileOnPublish – Cuando true , compila y se emite el ensamblado de Razor como parte de la
publicación del proyecto. Tiene como valor predeterminado true .
Las propiedades y elementos en la tabla siguiente se utilizan para configurar las entradas y salida para el SDK de
Razor.

ELEMENTOS DESCRIPCIÓN

RazorGenerate Elementos (archivos .cshtml) que son entradas para los


destinos de generación de código.

RazorCompile Elementos de elemento ( .cs archivos) que son entradas para


los destinos de compilación de Razor. Use esta ItemGroup
para especificar los archivos adicionales que se compilarán 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.microsoft.com/">

RazorEmbeddedResource Elementos que se agregan como recursos incrustados en el


ensamblado generado de Razor.
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. Valores válidos
son Implicit , RazorSDK y PrecompilationTool .

EnableDefaultContentItems El valor predeterminado es true . Cuando true , incluye


web.config, .json, y .cshtml archivos como contenido en el
proyecto. Cuando se hace referencia a través de
Microsoft.NET.Sdk.Web , los archivos en wwwroot y
también se incluyen los archivos de configuración.

EnableDefaultRazorGenerateItems Si es true , incluye los archivos .cshtml de los elementos


Content en elementos RazorGenerate .

GenerateRazorTargetAssemblyInfo Cuando true , genera un .cs archivo que contiene los


atributos especificados por RazorAssemblyAttribute e
incluye el archivo en la salida de compilación.

EnableDefaultRazorTargetAssemblyInfoAttributes Si es true , agrega a RazorAssemblyAttribute un


conjunto predeterminado de atributos de ensamblado.

CopyRazorGenerateFilesToPublishDirectory Cuando true , copias RazorGenerate elementos ( .cshtml)


archivos en el directorio de publicación. Normalmente, los
archivos de Razor no son necesarios para una aplicación
publicada si participa en la compilación en tiempo de
compilación o tiempo 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. Normalmente, los
ensamblados de referencia no son necesarios para una
aplicación publicada si la compilación de Razor se produce en
tiempo de compilación o tiempo de publicación. Establecido
en true si la aplicación publicada requiere compilación en
tiempo de ejecución. Por ejemplo, establezca el valor en
true si modifica la aplicación .cshtml archivos en tiempo de
ejecución o usa vistas incrustadas. Tiene como valor
predeterminado false .

IncludeRazorContentInPack Cuando true , todos los elementos de contenido de Razor (


.cshtml archivos) se marcan para su inclusión 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 .
Para más información sobre las propiedades, consulte Propiedades de MSBuild.
Destinos
El SDK de Razor establece dos objetivos principales:
RazorGenerate – Genera código .cs archivos desde RazorGenerate elementos de elemento. Use la propiedad
RazorGenerateDependsOn para especificar más destinos que puedan ejecutarse antes o después de este destino.
RazorCompile – Compila genera .cs archivos 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.

Para una aplicación web, asegúrese de que su aplicación tiene como destino el Microsoft.NET.Sdk.Web
SDK.

Versión de lenguaje Razor


Cuando el destino es el Microsoft.NET.Sdk.Web SDK, la versión de lenguaje de Razor se deduce de la versión de
framework de destino de la aplicación. Para los proyectos destinados a la Microsoft.NET.Sdk.Razor SDK o en el
caso excepcional de que la aplicación requiere una versión de lenguaje Razor diferente que el valor deducido, se
puede configurar una versión estableciendo el <RazorLangVersion> propiedad en el archivo de proyecto de la
aplicación:

<PropertyGroup>
<RazorLangVersion>{VERSION}</RazorLangVersion>
</PropertyGroup>

Recursos adicionales
Adiciones al formato csproj para .NET Core
Elementos comunes de proyectos de MSBuild
Información general de ASP.NET Core MVC
10/05/2019 • 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. 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.
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, saque la lógica de negocios fuera el controlador y llévela al modelo de dominio.

TIP
Si piensa que el controlador realiza con frecuencia los mismos tipos de acciones, mueva estas acciones comunes en 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
Asistentes 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>

Asistentes de etiquetas
Los asistentes 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 asistentes de etiquetas para definir etiquetas
personalizadas (por ejemplo, <environment> ) o para modificar el comportamiento de etiquetas existentes (por
ejemplo, <label> ). Los asistentes 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 muchos asistentes de etiquetas integradas para tareas comunes (como la creación de formularios, vínculos,
carga de activos, etc.) y existen muchos más a disposición en repositorios públicos de GitHub y como paquetes
NuGet. Los asistentes 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>

Los asistentes 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 los asistentes de etiquetas
integrados 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.
Creación de una aplicación web con MVC de
ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

En este tutorial se muestra el desarrollo web de ASP.NET Core MVC con controladores y vistas. Si todavía no
tiene experiencia en el desarrollo web de ASP.NET Core, considere la versión de Razor Pages de este tutorial, que
proporciona un punto de partida más sencillo.
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
04/07/2019 • 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. Si todavía no
tiene experiencia en el desarrollo web de ASP.NET Core, considere la versión de Razor Pages de este tutorial,
que proporciona un punto de partida más sencillo.
En este tutorial se enseñan los conceptos básicos de la compilación de una aplicación web ASP.NET Core MVC.
La aplicación administra una base de datos de títulos de películas. Aprenderá a:
Crear una aplicación web.
Agregar un modelo y aplicarle scaffolding.
Trabajar con una base de datos.
Agregar búsqueda y validación.
Al final, tendrá una aplicación que le permitirá administrar y mostrar datos de películas.
Vea o descargue el código de ejemplo (cómo descargarlo).

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work
with Visual Studio.

Creación de una aplicación web


Visual Studio
Visual Studio Code
Visual Studio para Mac
En Visual Studio, seleccione Crear un proyecto.
Seleccione Aplicación web de ASP.NET Core y, luego, Siguiente.
Asigne el nombre MvcMovie al proyecto y seleccione Crear. Es importante que el proyecto se llame
MvcMovie para que, al copiar el código, coincida con el espacio de nombres.

Seleccione Aplicación web (Modelo-Vista-Controlador) y, luego, Crear.


Visual Studio ha usado la 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.
Ejecutar la aplicación
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl-F5 para ejecutar la aplicación en modo de no depuración.
Visual Studio muestra el cuadro de diálogo siguiente:

Haga clic en Sí si confía en el certificado SSL de IIS Express.


Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener más información, vea Confiar en el certificado de desarrollo de ASP.NET Core HTTPS .
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.
El inicio de la aplicación con Ctrl+F5 (modo de no depuración) permite realizar 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 seleccionando el botón IIS Express.


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
Visual Studio Code
Visual Studio para Mac

Ayuda de Visual Studio


Información sobre cómo depurar código de C# con Visual Studio
Introducción al IDE de Visual Studio
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
18/06/2019 • 12 minutes to read • Edit Online

Por Rick Anderson


El patrón de arquitectura de Modelo-Vista-Controlador (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 (IU ) de la aplicación. Por lo
general, esta interfaz de usuario muestra los datos del modelo.
Controladores: 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, https://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). https://localhost:1234/Movies/Edit/5
es una solicitud para editar la película con ID=5 mediante el controlador de películas. Los datos de ruta se
explican más adelante en el 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.

Incorporación de un controlador
Visual Studio
Visual Studio Code
Visual Studio para Mac
En el Explorador de soluciones, haga clic con el botón derecho en Controladores > Agregar >
Controlador
.
En el cuadro de diálogo Agregar Scaffold, seleccione MVC Controller - Empty (Controlador MVC: en
blanco)

En el cuadro de diálogo Add Empty MVC Controller (Agregar controlador MVC en blanco), escriba
HelloWorldController y seleccione AGREGAR.
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 https://localhost:5001/HelloWorld . Combina el protocolo usado HTTPS , la ubicación de red del servidor
web (incluido el puerto TCP ) localhost:5001 y el URI de destino HelloWorld .
El primer comentario indica que se trata de un método HTTP GET que se invoca anexando /HelloWorld/ a la
dirección URL base. El segundo comentario especifica un método HTTP GET que se invoca anexando
/HelloWorld/Welcome/ a la dirección URL. Más adelante en el tutorial se usa el motor de scaffolding para generar
métodos HTTP POST que actualizan los datos.
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 de enrutamiento se establece en el método Configure del archivo Startup.cs.

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

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


predeterminada se usan 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. Los datos de ruta se explican más adelante en el tutorial.
Vaya a https://localhost:xxxx/HelloWorld/Welcome. El método Welcome se ejecuta y devuelve la cadena
This is the Welcome action method... . 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 (en concreto
JavaScript).
Usa cadenas interpoladas en $"Hello {name}, NumTimes is: {numTimes}" .

Ejecute la aplicación y navegue a:


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

(Reemplace xxxx con el número de puerto). Puede probar distintos valores 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 direcciones 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 siguiente dirección URL: https://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 elemento
? 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
03/07/2019 • 16 minutes to read • Edit Online

Por Rick Anderson


En esta sección, se modificará la clase HelloWorldController para usar los archivos de vista de 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 con 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 llama al método View del controlador. Este usa una plantilla de vista para generar una
respuesta HTML. Los métodos de controlador (también conocidos como métodos de acción), como el método
Index anterior, suelen devolver un valor IActionResult o una clase derivada de ActionResult, en lugar de un tipo
como una cadena string .

Agregar una vista


Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
Seleccione Vista de Razor.
Conserve el valor del cuadro Nombre, Index.cshtml.
Seleccione 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 https://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. Dado que no se especificó un nombre de archivo de plantilla de
vista, MVC usa el archivo de vista predeterminado. Este archivo tiene el mismo nombre que el método ( Index ),
por lo que se usa en /Views/HelloWorld/Index.cshtml. 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.
Cambio de vistas y páginas de diseño
Seleccione los vínculos de menú (MvcMovie [Película de MVC ], Home [Inicio] y Privacy [Privacidad]). 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 Privacy (Privacidad), la vista Views/Home/Privacy.cshtml se
representa dentro del método RenderBody .

Cambio de los vínculos del título, el pie de página y el menú en el


archivo de diseño
En los elementos de título y pie de página, cambie MvcMovie por Movie App .
Cambie el delimitador
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a> por
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a> .

En el marcado siguiente se muestran los cambios:

<!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 include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/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"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-
shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-
collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2019 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

@RenderSection("Scripts", required: false)


</body>
</html>

En el marcado anterior, se omitió el atributo del asistente de etiquetas delimitadoras asp-area porque esta
aplicación no utiliza Áreas.
Nota: El controlador Movies no se ha implementado. En este momento, el vínculo Movie App no es funcional.
Guarde los cambios y seleccione el vínculo Privacy (Privacidad). Observe cómo el título de la pestaña del
explorador muestra ahora Privacy Policy - Movie Ap (Directiva de privacidad - Aplicación de película) en
lugar de Privacy Policy - Mvc Movie (Directiva de privacidad - Aplicación de MVC ):
Pulse el vínculo Home (Inicio) y observe que el texto del título y el delimitador también muestran Movie App
(Aplicación de película). 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. Se 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 y el elemento <h2> del archivo de vista Views/HelloWorld/Index.cshtml:

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

<h2>My Movie List</h2>

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

El título y el elemento <h2> son algo diferentes para que pueda ver qué parte del código cambia la
presentación.
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 https://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.
En HelloWorldController.cs, 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 utilizarse
cualquier tipo; 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:


https://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, se usó 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 . Consulte When to use ViewBag, ViewData, or TempData ( Cuándo usar ViewBag, ViewData o
TempData) para más información.
En el tutorial siguiente crearemos 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
17/06/2019 • 23 minutes to read • Edit Online

Por Rick Anderson y Tom Dykstra


En esta sección, agregará las 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.
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 del modelo y EF Core crea la base de datos. Existe un enfoque
alternativo que no trataremos 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


Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Models > Agregar > Clase. Asigne a la clase el nombre Película.
Agregue las propiedades siguientes a la clase Movie :

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }

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

la clase Movie contiene:


El campo Id , que requiere la base de datos para la clave principal.
[DataType(DataType.Date)] : el atributo DataType especifica el tipo de datos ( Date ). Con este atributo:
El usuario no tiene que especificar información horaria en el campo de fecha.
Solo se muestra la fecha, no información horaria.
Los elementos DataAnnotations se tratan en un tutorial posterior.

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.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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, seleccione 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.
Seleccione 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.
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 usar para ello 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.

Migración inicial
En esta sección, se completan las tareas siguientes:
Agregar una migración inicial.
Actualizar la base de datos con la migración inicial.
Visual Studio
Visual Studio Code/Visual Studio para Mac
1. En el menú Herramientas, seleccione Administrador de paquetes NuGet > Consola del
Administrador de paquetes (PMC ).
2. En PCM, escriba los siguientes comandos:

Add-Migration Initial
Update-Database

El comando Add-Migration genera el código para crear el esquema de base de datos inicial.
El esquema de la base de datos se basa en el modelo especificado en la clase MvcMovieContext . El
argumento Initial es el nombre de la migración. Se puede usar cualquier nombre, pero, por
convención, se utiliza uno que describa la migración. Para obtener más información, vea Tutorial: Uso de
la característica de migraciones: ASP.NET MVC con EF Core.
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.

Examinar el contexto registrado con la inserción de dependencias


ASP.NET Core integra la inserción de dependencias (DI). 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.
Visual Studio
Visual Studio Code/Visual Studio para Mac
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.
Consulte el siguiente método Startup.ConfigureServices . 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_2);

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

El elemento MvcMovieContext coordina la funcionalidad de EF Core (creación, lectura, actualización, eliminación,


etc.) para el modelo Movie . El contexto de datos ( MvcMovieContext ) se deriva de
Microsoft.EntityFrameworkCore.DbContext. En el contexto de datos se especifica qué entidades se incluyen en el
modelo de datos:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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; }


}
}

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.
Prueba de la aplicación
Ejecute la aplicación y anexe /Movies a la dirección URL en el explorador ( http://localhost:port/movies ).

Si se produce una excepción de base de datos similar a la siguiente:

SqlException: Cannot open database "MvcMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.

Quiere decir que falta el paso de migraciones.


Pruebe el vínculo Crear. Escriba y envíe los datos.
NOTE
Es posible que no pueda escribir comas decimales en el campo Price . La aplicación debe globalizarse 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. Para obtener instrucciones sobre la
globalización, consulte esta cuestión en GitHub.

Pruebe los vínculos Editar, Detalles y Eliminar.


Examine la clase Startup :

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

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

En el código resaltado anterior se muestra cómo se agrega el contexto de base de datos de películas al
contenedor de inserción de dependencias:
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);
}

El parámetro id suele pasarse como datos de ruta. Por ejemplo, https://localhost:5001/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:
https://localhost:5001/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);

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

<h1>Details</h1>

<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Genre)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class="col-sm-10">
@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. Al crear el controlador de película, se 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";
}

<h1>Index</h1>

<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 implica 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
Trabajo con SQL en ASP.NET Core
10/05/2019 • 8 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:
Visual Studio
Visual Studio Code/Visual Studio para Mac

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

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.
Visual Studio
Visual Studio Code/Visual Studio para Mac

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

Prueba de la aplicación
Visual Studio
Visual Studio Code/Visual Studio para Mac
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
10/05/2019 • 16 minutes to read • Edit Online

Por Rick Anderson


La aplicación de películas tiene buena pinta, pero la presentación no es la ideal. Por ejemplo,
FechaDeLanzamiento debería escribirse en tres 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; }
}
}

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 https://localhost:5001/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 obtener
más información, consulte 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, consulte 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 FindAsync 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);
}

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

<h1>Edit</h1>

<h4>Movie</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="Id" />
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

<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 publique en el servidor, la validación del lado cliente comprueba las reglas 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
10/05/2019 • 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?}");
});

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 el asistente 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 el asistente 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, se
especifica que la solicitud sea HTTP GET :
@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>

<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)

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">

Adición de 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 { get; set; }
public SelectList Genres { get; set; }
public string MovieGenre { get; set; }
public string SearchString { 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 permite al usuario seleccionar un género de la lista.
MovieGenre , que contiene el género seleccionado.
SearchString , que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

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

// GET: Movies
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


{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
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).
Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice


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

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

<h1>Index</h1>

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

En el código anterior, 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. 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 buscando por género, por título de la película y por 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
18/06/2019 • 9 minutes to read • Edit Online

Por Rick Anderson


En esta sección, Migraciones de Entity Framework Code First se utiliza para:
Agregar un campo nuevo al modelo.
Migrar el nuevo campo a la base de datos.
Al usar Code First de EF para crear una base de datos automáticamente, Code First hace lo siguiente:
Agrega una tabla a la base de datos para realizar un seguimiento del esquema de la base de datos.
Comprueba que la base de datos está sincronizada con las clases del modelo desde las 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


Agregue una Rating propiedad a Models/Movie.cs:

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


Dado que ha agregado un nuevo campo a la clase Movie , debe actualizar la lista de enlaces permitidos 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")]

Actualice 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 :
<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>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |

Actualice /Views/Movies/Create.cshtml con un campo Rating .


Visual Studio/Visual Studio para Mac
Visual Studio Code
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.
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
},

La aplicación no funciona hasta que la base de datos se actualiza para incluir el nuevo campo. Si se ejecuta
ahora, se produce la siguiente SqlException :
SqlException: Invalid column name 'Rating'.

Este error se produce porque la clase del modelo Movie actualizada es diferente a la 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. 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 realiza 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. Se trata de un buen enfoque para el desarrollo inicial y
cuando se usa SQLite.
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.
En este tutorial se usa Migraciones de Code First.
Visual Studio
Visual Studio Code/Visual Studio para Mac
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 se eliminan todos los registros de la base de datos, el método de inicialización inicializa la base de datos e
incluye el campo Rating .
Ejecute la aplicación y compruebe que puede crear, editar o mostrar películas con un campo Rating . 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
17/06/2019 • 17 minutes to read • Edit Online

Por Rick Anderson


En esta sección:
Se agrega lógica de validación al modelo Movie .
Asegúrese de que las reglas de validación se aplican cada vez 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.

Add validation rules to the movie model


Open the Movie.cs file. The DataAnnotations namespace provides a set of built-in validation attributes that are
applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType
that help with formatting and don't provide any validation.
Update the Movie class to take advantage of the built-in Required , StringLength , RegularExpression , and
Range validation attributes.
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; }
}

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing
prevents a user from entering white space to satisfy this validation.
The RegularExpression attribute is used to limit what characters can be input. In the preceding code,
"Genre":
Must only use letters.
The first letter is required to be uppercase. White space, numbers, and special characters are not
allowed.
The RegularExpression "Rating":
Requires that the first character be an uppercase letter.
Allows special characters and numbers in subsequent spaces. "PG -13" is valid for a rating, but fails for
a "Genre".
The Range attribute constrains a value to within a specified range.
The StringLength attribute lets you set the maximum length of a string property, and optionally its
minimum length.
Value types (such as decimal , int , float , DateTime ) are inherently required and don't need the
[Required] attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also
ensures that you can't forget to validate something and inadvertently let bad data into the database.

UI de error de validación
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 campos decimales. 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.

La parte de la plantilla de visualización Create.cshtml se muestra en el marcado siguiente:


<h4>Movie</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="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>

@*Markup removed for brevity.*@

Los métodos de acción utilizan el marcado anterior para mostrar el formulario inicial y para volver a mostrarlo
en caso de error.
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; }
}

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
10/05/2019 • 5 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);
}

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 FirstOrDefaultAsync . 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));
}

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 los segmentos de una dirección URL a los métodos de acción por nombre, de modo que si
cambia el nombre de un método, el enrutamiento seguramente no podrá encontrar dicho 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
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

Publicar en Azure
Para obtener información sobre la implementación en Azure, consulte Tutorial: Creación de una aplicación .NET
Core y SQL Database en Azure App Service.

A N T E R IO R
Vistas de ASP.NET Core MVC
10/05/2019 • 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 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 del asistente 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 del asistente 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. Nota: Las búsquedas de claves no distinguen mayúsculas de minúsculas para ViewData y ViewBag .
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


Los asistentes de etiquetas permiten agregar fácilmente el comportamiento del lado servidor a las etiquetas
HTML existentes. El uso de asistentes de etiquetas evita la necesidad de escribir código personalizado o
asistentes en las vistas. Los asistentes 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 muchos asistentes 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.
Vistas parciales en ASP.NET Core
18/06/2019 • 17 minutes to read • Edit Online

Por Steve Smith, Luke Latham, Maher JENDOUBI, Rick Anderson y Scott Sauber
Una vista parcial es un archivo de marcado Razor (.cshtml) que representa una salida HTML dentro de otra
salida representada del archivo de marcado.
El término vista parcial se usa al desarrollar una aplicación MVC, donde a los archivos de marcado se les
denomina vistas, o una aplicación de Razor Pages, donde a los archivos de marcado se les denomina páginas.
En este tema se hace referencia genéricamente a las vistas de MVC y a las páginas de Razor Pages como
archivos de marcado.
Vea o descargue el código de ejemplo (cómo descargarlo)

¿Cuándo se usan las vistas parciales?


Las vistas parciales son una forma eficaz de:
Dividir los archivos de marcado grandes en componentes más pequeños.
En un gran archivo de marcado complejo formado por varias partes lógicas, se ofrece la ventaja de
trabajar con cada pieza aislada en una vista parcial. El código del archivo de marcado es fácil de
administrar porque el marcado solo contiene la estructura general de la página y las referencias a las
vistas parciales.
Reducir la duplicación de contenido de marcado común en todos los archivos de marcado.
Cuando se usan los mismos elementos de marcado en todos los archivos de marcado, una vista parcial
elimina la duplicación del contenido de marcado en un archivo de vista parcial. Cuando se cambia el
marcado en la vista parcial, actualiza la salida representada de los archivos de marcado que utilizan la
vista parcial.
Las vistas parciales no deben utilizarse para mantener los elementos de diseño comunes. Se deben especificar
elementos de diseño comunes en los archivos _Layout.cshtml.
No utilice una vista parcial donde se requiera la ejecución de código o lógica de representación compleja para
representar el marcado. En lugar de una vista parcial, use un componente de vista.

Declaración de vistas parciales


Una vista parcial es un archivo de marcado .cshtml que se mantiene dentro de la carpeta Vistas (MVC ) o
Páginas (Razor Pages).
En ASP.NET Core MVC, la clase ViewResult de un controlador es capaz de devolver una vista o una vista
parcial. En Razor Pages, un elemento PageModel puede devolver una vista parcial representada como un
objeto PartialViewResult. La referencia y representación de vistas parciales se describe en la sección
Referencia a una vista parcial.
A diferencia de la representación de páginas o vistas de MVC, una vista parcial no ejecuta _ViewStart.cshtml.
Para más información sobre _ViewStart.cshtml, vea Diseño en ASP.NET Core.
Los nombres de archivo de las vistas parciales suelen comenzar con un guión bajo ( _ ). Esta convención de
nomenclatura no es obligatoria, pero ayuda a diferenciar visualmente las vistas parciales de las vistas y las
páginas.
Una vista parcial es un archivo de marcado .cshtml que se mantiene dentro de la carpeta Vistas.
La clase ViewResult de un controlador es capaz de devolver una vista o una vista parcial. La referencia y
representación de vistas parciales se describe en la sección Referencia a una vista parcial.
A diferencia de la representación de vistas de MVC, una vista parcial no ejecuta _ViewStart.cshtml. Para más
información sobre _ViewStart.cshtml, vea Diseño en ASP.NET Core.
Los nombres de archivo de las vistas parciales suelen comenzar con un guión bajo ( _ ). Esta convención de
nomenclatura no es obligatoria, pero ayuda a diferenciar visualmente las vistas parciales de las vistas.

Referencia a una vista parcial


Uso de una vista parcial en una clase PageModel de Razor Pages
En ASP.NET Core 2.0 o 2.1, el método de controlador siguiente representa la vista parcial
_AuthorPartialRP.cshtml en la respuesta:

public IActionResult OnGetPartial() =>


new PartialViewResult
{
ViewName = "_AuthorPartialRP",
ViewData = ViewData,
};

En ASP.NET Core 2.2 o posterior, un método de controlador puede llamar de forma alternativa al método
Partial para generar un objeto PartialViewResult :

public IActionResult OnGetPartial() =>


Partial("_AuthorPartialRP");

Uso de una vista parcial en un archivo de marcado


Dentro de un archivo de marcado, hay varias maneras de hacer referencia a una vista parcial. Se recomienda
que las aplicaciones usen uno de los siguientes métodos de representación asincrónica:
Asistente de etiquetas parciales
Asistente de HTML asincrónico
Dentro de un archivo de marcado, hay dos formas de hacer referencia a una vista parcial:
Asistente de HTML asincrónico
Asistente de HTML sincrónico
Se recomienda que las aplicaciones usen el Asistente de HTML asincrónico.
Asistente de etiquetas parciales
El asistente de etiquetas parciales requiere ASP.NET Core 2.1 o posterior.
El asistente de etiquetas parciales representa contenido de forma asincrónica y usa una sintaxis similar a
HTML:

<partial name="_PartialName" />

Cuando hay una extensión de archivo, el asistente de etiquetas hace referencia a una vista parcial que debe
estar en la misma carpeta que el archivo de marcado que llama a la vista parcial:
<partial name="_PartialName.cshtml" />

En el ejemplo siguiente se hace referencia a una vista parcial desde la raíz de la aplicación. Las rutas de acceso
que comienzan con una tilde de la ñ y una barra diagonal ( ~/ ) o una barra diagonal ( / ) hacen referencia a la
raíz de la aplicación:
Páginas de Razor

<partial name="~/Pages/Folder/_PartialName.cshtml" />


<partial name="/Pages/Folder/_PartialName.cshtml" />

MVC

<partial name="~/Views/Folder/_PartialName.cshtml" />


<partial name="/Views/Folder/_PartialName.cshtml" />

En el ejemplo siguiente se hace referencia a una vista parcial con una ruta de acceso relativa:

<partial name="../Account/_PartialName.cshtml" />

Para obtener más información, vea Asistente de etiquetas parciales en ASP.NET Core.
Asistente de HTML asincrónica
Cuando se usa un asistente de HTML, el procedimiento recomendado es usar PartialAsync. PartialAsync
devuelve un tipo IHtmlContent encapsulado en una clase Task<TResult>. Para hacer referencia al método, se
agrega a la llamada awaited un carácter @ como prefijo:

@await Html.PartialAsync("_PartialName")

Cuando la extensión del archivo está presente, el asistente de HTML hace referencia a una vista parcial que
debe estar en la misma carpeta que el archivo de marcado que llama a la vista parcial:

@await Html.PartialAsync("_PartialName.cshtml")

En el ejemplo siguiente se hace referencia a una vista parcial desde la raíz de la aplicación. Las rutas de acceso
que comienzan con una tilde de la ñ y una barra diagonal ( ~/ ) o una barra diagonal ( / ) hacen referencia a la
raíz de la aplicación:
Páginas de Razor

@await Html.PartialAsync("~/Pages/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Pages/Folder/_PartialName.cshtml")

MVC

@await Html.PartialAsync("~/Views/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Views/Folder/_PartialName.cshtml")

En el ejemplo siguiente se hace referencia a una vista parcial con una ruta de acceso relativa:
@await Html.PartialAsync("../Account/_LoginPartial.cshtml")

También puede representar una vista parcial con RenderPartialAsync. Este método no devuelve IHtmlContent.
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");
}

Puesto que RenderPartialAsync transmite contenido representado, ofrece mayor rendimiento en algunos
escenarios. En situaciones críticas de rendimiento, realice pruebas comparativas de la página con ambos
métodos y use el que genera una respuesta más rápida.
Asistente de HTML sincrónico
Partial y RenderPartial son los equivalentes sincrónicos de PartialAsync y RenderPartialAsync ,
respectivamente. Los equivalentes sincrónicos no son aconsejables, ya que hay escenarios donde producen
interbloqueos. Se prevé la eliminación de los métodos sincrónicos en una futura versión.

IMPORTANT
Si no necesita ejecutar código, use un componente de vista en lugar de una vista parcial.

La llamada a Partial o RenderPartial resulta en una advertencia del analizador de Visual Studio. Por
ejemplo, la presencia de Partial genera el siguiente mensaje de advertencia:

Use of IHtmlHelper.Partial may result in application deadlocks. Considere la posibilidad de utilizar el


asistente de etiquetas <parciales> o IHtmlHelper.PartialAsync.

Reemplace las llamadas a @Html.Partial por @await Html.PartialAsync o el asistente de etiquetas parciales.
Para más información sobre la migración del asistente de etiquetas parciales, vea Migración desde un
asistente de HTML.

Detección de vistas parciales


Cuando se hace referencia a una vista parcial por su nombre sin una extensión de archivo, se busca en las
siguientes ubicaciones en el orden indicado:
Páginas de Razor
1. Carpeta de la página en ejecución actualmente
2. Gráfico de directorio por encima de la carpeta de la página
3. /Shared
4. /Pages/Shared
5. /Views/Shared

MVC
1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared
4. /Pages/Shared
1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared

Las convenciones siguientes se aplican a la detección de la vista parcial:


Se admiten diferentes vistas parciales con el mismo nombre de archivo cuando las vistas parciales están en
carpetas diferentes.
Al hacer referencia a una vista parcial por su nombre sin una extensión de archivo y la vista parcial está
presente tanto en la carpeta del autor de la llamada como en la carpeta compartida, la vista parcial de la
carpeta del autor de la llamada proporciona la vista parcial. Si la vista parcial no está presente en la carpeta
del autor de la llamada, se proporciona la vista parcial desde la carpeta compartida. Las vistas parciales de
la carpeta compartida se denominan vistas parciales compartidas o vistas parciales predeterminadas.
Las vistas parciales pueden estar encadenadas: una vista parcial puede llamar a otra vista parcial si las
llamadas no forman una referencia circular. Las rutas de acceso relativas siempre guardan relación con el
archivo actual, no con la raíz ni el elemento primario del archivo.

NOTE
Una clase section de Razor definida en una vista parcial no es visible para los archivos de marcado 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, recibe una copia del diccionario ViewData del elemento
primario. 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.
En el ejemplo siguiente se muestra cómo pasar una instancia de ViewDataDictionary a una vista parcial:

@await Html.PartialAsync("_PartialName", customViewData)

Puede pasar un modelo a una vista parcial. El modelo puede ser un objeto personalizado. Puede pasar un
modelo con PartialAsync (representa un bloque de contenido al autor de la llamada) o RenderPartialAsync
(transmite el contenido a la salida):

@await Html.PartialAsync("_PartialName", model)

Páginas de Razor
El siguiente marcado de la aplicación de ejemplo proviene de la página Pages/ArticlesRP/ReadRP.cshtml. La
página contiene dos vistas parciales. La segunda vista parcial se pasa a un modelo y ViewData a la vista
parcial. La sobrecarga del constructor de ViewDataDictionary se usa para pasar un nuevo diccionario
ViewData a la vez que conserva el diccionario ViewData existente.
@model ReadRPModel

<h2>@Model.Article.Title</h2>
@* Pass the author's name to Pages\Shared\_AuthorPartialRP.cshtml *@
@await Html.PartialAsync("../Shared/_AuthorPartialRP", Model.Article.AuthorName)
@Model.Article.PublicationDate

@* Loop over the Sections and pass in a section and additional ViewData to
the strongly typed Pages\ArticlesRP\_ArticleSectionRP.cshtml partial view. *@
@{
var index = 0;

foreach (var section in Model.Article.Sections)


{
await Html.PartialAsync("_ArticleSectionRP",
section,
new ViewDataDictionary(ViewData)
{
{ "index", index }
});

index++;
}
}

Pages/Shared/_AuthorPartialRP.cshtml es la primera vista parcial a la que hace referencia el archivo de


marcado ReadRP.cshtml:

@model string
<div>
<h3>@Model</h3>
This partial view from /Pages/Shared/_AuthorPartialRP.cshtml.
</div>

Pages/ArticlesRP/_ArticleSectionRP.cshtml es la segunda vista parcial a la que hace referencia el archivo de


marcado ReadRP.cshtml:

@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"]</h3>


<div>
@Model.Content
</div>

MVC
El marcado siguiente de la aplicación de ejemplo muestra la vista Views/Articles/Read.cshtml. La vista contiene
dos vistas parciales. La segunda vista parcial se pasa a un modelo y ViewData a la vista parcial. La sobrecarga
del constructor de ViewDataDictionary se usa para pasar un nuevo diccionario 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.cshtml es la primera vista parcial a la que hace referencia el archivo de marcado


ReadRP.cshtml:

@model string
<div>
<h3>@Model</h3>
This partial view from /Views/Shared/_AuthorPartial.cshtml.
</div>

Views/Articles/_ArticleSection.cshtml es la segunda vista parcial a la que hace referencia el archivo de marcado


Read.cshtml:

@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 salida representada del archivo de marcado
principal, que a su vez se representa dentro de _Layout.cshtml compartido. La primera vista parcial representa
la fecha de publicación y el nombre del autor del artículo:

Abraham Lincoln
Esta vista parcial procede de la <ruta de acceso de archivo de la vista parcial compartida>. 19/11/1863
12:00:00 a. m.

La segunda vista parcial representa las secciones del artículo:

Índice de la sección uno: 0


Puntuación de cuatro y hace siete años...
Índice de la sección dos: 1
Ahora nos encontramos en una verdadera guerra civil, probando...
Índice de la sección tres: 2
Pero, en un sentido amplio, no nos podemos dedicar...

Recursos adicionales
Referencia de sintaxis de Razor para ASP.NET Core
Asistentes de etiquetas en ASP.NET Core
Asistente de etiquetas parciales en ASP.NET Core
Componentes de vista en ASP.NET Core
Áreas de ASP.NET Core
Referencia de sintaxis de Razor para ASP.NET Core
Componentes de vista en ASP.NET Core
Áreas de ASP.NET Core
Control de solicitudes con controladores en ASP.NET
Core MVC
10/05/2019 • 10 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 del asistente 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 del asistente:
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 del
asistente 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 del asistente 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 PhysicalFile. Por ejemplo,
return PhysicalFile(customerFilePath, "text/xml"); devuelve PhysicalFileResult.
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 del asistente 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.
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
10/05/2019 • 60 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 asistentes específicos 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 / o ~/ 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 .

El enrutamiento del controlador de MVC y el enrutamiento de Razor Pages comparten una implementación.
La información sobre el orden de la ruta de los temas de Razor Pages se encuentra disponible en
Convenciones de aplicación y de ruta de Razor Pages: Orden de la ruta.

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 reemplazan con 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 coinciden 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]")] genera 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.
Usar un transformador de parámetro para personalizar el reemplazo de tokens
El reemplazo de tokens se puede personalizarse mediante un transformador de parámetro. Un
transformador de parámetro implementa IOutboundParameterTransformer y transforma el valor de
parámetros. Por ejemplo, un transformador de parámetros SlugifyParameterTransformer personalizado
cambia el valor de ruta SubscriptionManagement a subscription-management .
RouteTokenTransformerConvention es una convención de modelo de aplicación que:
Aplica un transformador de parámetro a todas las rutas de atributo en una aplicación.
Personaliza los valores de token de ruta de atributo que se reemplazan.

public class SubscriptionManagementController : Controller


{
[HttpGet("[controller]/[action]")] // Matches '/subscription-management/list-all'
public IActionResult ListAll() { ... }
}

RouteTokenTransformerConvention está registrado como una opción en ConfigureServices .

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});
}

public class SlugifyParameterTransformer : IOutboundParameterTransformer


{
public string TransformOutbound(object value)
{
if (value == null) { return null; }

// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}

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.

Segmentos complejos
Los segmentos complejos (por ejemplo, [Route("/dog{token}cat")] ), se procesan buscando coincidencias de
literales de derecha a izquierda de un modo no expansivo. Para ver una descripción, eche un vistazo al
código fuente. Para más información, vea este problema.

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
10/05/2019 • 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
10/05/2019 • 3 minutes to read • Edit Online

Por Shadi Namrouti, Rick Anderson y Steve Smith


Los controladores de ASP.NET Core MVC solicitan las dependencias de forma explícita a través de
constructores. ASP.NET Core tiene compatibilidad integrada con la inserción de dependencias. La inserción de
dependencias facilita las pruebas y el mantenimiento de las aplicaciones.
Vea o descargue el código de ejemplo (cómo descargarlo)

Inserción de constructores
Los servicios se agregan como un parámetro de constructor y el runtime resuelve el servicio desde el
contenedor de servicios. Normalmente, los servicios se definen mediante interfaces. Por ejemplo, considere una
aplicación que requiere la hora actual. En la interfaz siguiente se expone el servicio IDateTime :

public interface IDateTime


{
DateTime Now { get; }
}

En el código siguiente se implementa la interfaz IDateTime :

public class SystemDateTime : IDateTime


{
public DateTime Now
{
get { return DateTime.Now; }
}
}

Agregue el servicio al contenedor de servicios:

public void ConfigureServices(IServiceCollection services)


{
services.AddSingleton<IDateTime, SystemDateTime>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Para más información sobre AddSingleton, vea Duraciones de servicio de DI.


En el código siguiente se muestra un saludo al usuario según la hora del día:
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();
}

Ejecute la aplicación y se mostrará un mensaje en función de la hora.

Inserción de acción con FromServices


FromServicesAttribute permite la inserción de un servicio directamente en un método de acción sin usar la
inserción de constructores:

public IActionResult About([FromServices] IDateTime dateTime)


{
ViewData["Message"] = $"Current server time: {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. El patrón de opciones que se describe en Patrón de opciones en ASP.NET Core es el enfoque preferido
para administrar la configuración. Por lo general, IConfiguration no se inserta directamente en un controlador.
Cree una clase que represente las opciones. Por ejemplo:

public class SampleWebSettings


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

Agregue la clase de configuración a la colección de servicios:


public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDateTime, SystemDateTime>();
services.Configure<SampleWebSettings>(Configuration);

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Configure la aplicación para leer la configuración de un archivo con formato JSON:

public class Program


{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("samplewebsettings.json",
optional: false, // File is not optional.
reloadOnChange: false);
})
.UseStartup<Startup>();
}

En el código siguiente se solicita la configuración IOptions<SampleWebSettings> del contenedor de servicios y se


usa en el método Index :

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

Recursos adicionales
Vea Probar la lógica del controlador en ASP.NET Core para obtener información sobre cómo solicitar
explícitamente dependencias en controladores para facilitar la comprobación del código.
Reemplazo del contenedor de inserción de dependencias predeterminado con una implementación de
terceros.
Inserción de dependencias en vistas de ASP.NET
Core
17/05/2019 • 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)

Inserción de configuración
Los valores appSettings.JSON se pueden insertar directamente en una vista.
Ejemplo de un archivo appsettings.json:

{
"root": {
"parent": {
"child": "myvalue"
}
}
}

La sintaxis de @inject : @inject <type> <name>

Un ejemplo que usa @inject :

@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
string myValue = Configuration["root:parent:child"];
...
}

Inserción de un servicio
Un servicio puede insertarse 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.
@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 los asistentes 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)
Probar la lógica del controlador en ASP.NET Core
10/05/2019 • 19 minutes to read • Edit Online

Por Steve Smith


Los controles desempeñan un rol fundamental en cualquier aplicación de ASP.NET Core MVC. Por tanto, debe
tener la seguridad de que los controladores se comportan según lo previsto. Las pruebas automatizadas pueden
detectar errores antes de que la aplicación se implemente en un entorno de producción.
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í.
Configure pruebas unitarias de las acciones del controlador para centrarse en el comportamiento del controlador.
Una prueba unitaria del controlador evita escenarios como filtros, enrutamiento y enlace de modelos. Las pruebas
que abarcan las interacciones entre los componentes que colectivamente responden a una solicitud se controlan
mediante pruebas de integración. Para más información sobre las pruebas de integración, vea Pruebas de
integración en ASP.NET Core.
Si va a escribir filtros personalizados y rutas, realice pruebas unitarias en ellos de forma aislada, no como parte de
las pruebas de una acción de controlador concreta.
Para demostrar las pruebas unitarias del controlador, revise el siguiente controlador en la aplicación de ejemplo. El
controlador Home muestra una lista de sesiones de lluvia de ideas y permite crear nuevas sesiones de lluvia de
ideas con una solicitud POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public HomeController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index()


{
var sessionList = await _sessionRepository.ListAsync();

var model = sessionList.Select(session => new StormSessionViewModel()


{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});

return View(model);
}

public class NewSessionModel


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

[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}

return RedirectToAction(actionName: nameof(Index));


}
}

El controlador anterior:
Sigue el Principio de dependencias explícitas.
Espera la inserción de dependencias (DI) para ofrecer una instancia de IBrainstormSessionRepository .
Se puede probar con un servicio IBrainstormSessionRepository ficticio con el uso de un marco de objeto ficticio,
como Moq. Un objeto ficticio es un objeto fabricado con un conjunto predeterminado de comportamientos de
propiedades y métodos utilizados para las pruebas. Para más información, vea Introducción a las pruebas de
integración.
El método HTTP GET Index no tiene bucles ni bifurcaciones y solamente llama a un método. La prueba unitaria para
esta acción:
Realice el simulacro del servicio IBrainstormSessionRepository mediante el método GetTestSessions .
GetTestSessions crea dos sesiones de lluvia de ideas ficticias con fechas y nombres de sesión.
Ejecuta el método Index .
Realiza aserciones sobre el resultado devuelto por el método:
Se devuelve ViewResult.
ViewDataDictionary.Model es StormSessionViewModel .
Hay dos sesiones de lluvia de ideas almacenadas en ViewDataDictionary.Model .

[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);
Assert.Equal(2, model.Count());
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}

Las pruebas del método HTTP POST Index del controlador Home verifican que:
Si ModelState.IsValid es false , el método de acción devuelve ViewResult de 400 Solicitud incorrecta con los
datos apropiados.
Cuando ModelState.IsValid es true :
Se llama al método Add del repositorio.
Se devuelve RedirectToActionResult con los argumentos correctos.
Un estado de modelo no válido se comprueba mediante la adición de errores con AddModelError, como se
muestra en la primera prueba de abajo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.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
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}

Si ModelState no es válido, se devuelve el mismo elemento ViewResult que para una solicitud GET. La prueba no
intenta pasar un modelo no válido. Pasar un modelo no válido no es un enfoque válido, ya que el enlace de
modelos no está en ejecución (aunque una prueba de integración usa el enlace de modelos). En este caso, no se
comprueba el enlace de modelos. Estas pruebas unitarias solo comprueban el código del método de acción.
La segunda prueba verifica que, cuando ModelState es válido:
Se agrega un nuevo elemento BrainstormSession (mediante el repositorio).
El método devuelve un elemento RedirectToActionResult con las propiedades esperadas.

Las llamadas ficticias que no se efectúan se suelen ignorar, aunque llamar a Verifiable al final de la llamada de
configuración permite realizar una validación ficticia de la prueba. Esto se realiza 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 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.

SessionController en la aplicación de ejemplo muestra información relacionada con una sesión de lluvia de ideas
determinada. El controlador incluye lógica para tratar valores id no válidos (hay dos escenarios return en el
ejemplo siguiente para abarcar estos escenarios). La última instrucción return devuelve un StormSessionViewModel
nuevo a la vista (Controllers/SessionController.cs):

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

Las pruebas unitarias incluyen una prueba de cada escenario return en la acción Index del controlador de sesión:
[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);
}

Al pasar al controlador de ideas, la aplicación expone la funcionalidad como una API web en la ruta api/ideas :
El método ForSession devuelve una lista de ideas ( IdeaDTO ) asociadas con una sesión de lluvia de ideas.
El método Create agrega nuevas ideas a una sesión.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);


if (session == null)
{
return NotFound(model.SessionId);
}

var idea = new Idea()


{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return Ok(session);
}

Evite devolver entidades de dominio de negocio directamente mediante llamadas API. Entidades de dominio:
Suelen incluir más datos de los que necesita el cliente.
Innecesariamente acoplan el modelo de dominio interno de la aplicación con la API expuesta públicamente.
La asignación entre las entidades de dominio y los tipos devueltos al cliente se puede realizar:
Manualmente, con una operación Select de LINQ, como la aplicación de ejemplo usa. Para más información,
vea LINQ (Language Integrated Query).
Automáticamente, con una biblioteca, como AutoMapper.
A continuación, la aplicación de ejemplo muestra las pruebas unitarias para los métodos de API Create y
ForSession del controlador de ideas.

La aplicación de ejemplo contiene dos pruebas ForSession . La primera prueba determina si ForSession devuelve
NotFoundObjectResult (HTTP no encontrado) para una sesión no válida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSession(testSessionId);

// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

La segunda prueba ForSession determina si ForSession devuelve una lista de ideas de sesión ( <List<IdeaDTO>> )
para una sesión válida. Las comprobaciones también examinan la primera idea para confirmar si su propiedad
Name es correcta:

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

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

Para comprobar el comportamiento del método Create cuando ModelState no es válido, la aplicación de ejemplo
agrega 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 del método de acción al
confrontarlo con un valor de ModelState no válido:

[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);
}
La segunda prueba de Create depende de que el repositorio devuelva null , por lo que el repositorio ficticio está
configurado para devolver 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. La prueba puede realizarse en una sola instrucción,
como se muestra en el código de ejemplo:

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
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);
}

La tercera prueba de Create , Create_ReturnsNewlyCreatedIdeaForSession , verifica que se llama al método


UpdateAsync del repositorio. Se llama al objeto ficticio con Verifiable y, después, se llama al método Verify del
repositorio ficticio para confirmar que se ejecutó el método Verifiable. La prueba unitaria no es responsable de
garantizar que el método UpdateAsync guarda los datos; esto se puede realizar con una prueba de integración.

[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();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Probar ActionResult<T>
En ASP.NET Core 2.1 o posterior, ActionResult<T> (ActionResult<TValue>) permite devolver un tipo que se deriva
de ActionResult o bien un tipo específico.
La aplicación de ejemplo incluye un método que devuelve un resultado List<IdeaDTO> para una sesión
determinada id . Si la sesión id no existe, el controlador devuelve NotFound:

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

Dos pruebas del controlador ForSessionActionResult se incluyen en ApiIdeasControllerTests .


La primera prueba confirma que el controlador devuelve ActionResult , pero no una lista inexistente de ideas para
una sesión inexistente id :
El tipo ActionResult es ActionResult<List<IdeaDTO>> .
Result es NotFoundObjectResult.

[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;

// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

Para obtener un elemento id de sesión válido, la segunda prueba confirma que el método devuelve:
Un ActionResult con un tipo List<IdeaDTO> .
ActionResult<T>.Value es de un tipo List<IdeaDTO> .
El primer elemento de la lista es una idea válida que coincide con la idea almacenada en la sesión ficticia
(obtenido mediante una llamada a GetTestSession ).

[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSessionActionResult(testSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}

La aplicación de ejemplo también incluye un método para crear un nuevo elemento Idea para una sesión
determinada. El controlador devuelve:
BadRequest para un modelo no válido.
NotFound si no existe la sesión.
CreatedAtAction cuando se actualiza la sesión con la idea nueva.

[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()


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


}

Tres pruebas de CreateActionResult se incluyen en ApiIdeasControllerTests .


El primer texto confirma que se devuelve BadRequest para un modelo no válido.
[Fact]
public async Task CreateActionResult_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.CreateActionResult(model: null);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

La segunda prueba comprueba que se devuelve NotFound si la sesión no existe.

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};

// Act
var result = await controller.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

Para una sesión válida id , la prueba final confirma que:


El método devuelve ActionResult con un tipo BrainstormSession .
ActionResult<T>.Result es CreatedAtActionResult. CreatedAtActionResult es similar a una respuesta 201
Creado con un encabezado Location .
ActionResult<T>.Value es un tipo BrainstormSession .
La llamada ficticia para actualizar la sesión, UpdateAsync(testSession) , se ha invocado. La llamada al método
Verifiable se comprueba mediante la ejecución de mockRepo.Verify() en las aserciones.
Se devuelven dos objetos Idea para la sesión.
El último elemento (el elemento Idea agregado por la llamada ficticia a UpdateAsync ) coincide con el elemento
newIdea agregado a la sesión en la prueba.
[Fact]
public async Task CreateActionResult_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.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

Recursos adicionales
Pruebas de integración en ASP.NET Core
Cree y ejecute pruebas unitarias con Visual Studio.
Introducción a Blazor
02/07/2019 • 10 minutes to read • Edit Online

Por Daniel Roth y Luke Latham


Le damos la bienvenida a Blazor.
Blazor es un marco de trabajo para la creación de interfaces de usuario web interactivas del lado cliente con
.NET:
Cree interfaces de usuario completamente interactivas con C# en lugar de JavaScript.
Comparta la lógica de aplicación del lado cliente y servidor escrita con .NET.
Represente la interfaz de usuario como HTML y CSS para la compatibilidad con todos los exploradores,
incluidos los móviles.
El uso de .NET para el desarrollo web en el lado cliente ofrece las siguientes ventajas:
escriba código en C# en lugar de JavaScript.
Aprovechamiento del ecosistema y las bibliotecas .NET existentes.
Uso compartido de la lógica de aplicación en el servidor y el cliente.
Beneficios de rendimiento, confiabilidad y seguridad de .NET.
mantenga la productividad con Visual Studio en Windows, Linux y macOS.
compile sobre un conjunto común de lenguajes, marcos y herramientas que son estables, completos y fáciles
de usar.

Componentes
Las aplicaciones de Blazor se basan en componentes. Un componente en Blazor es un elemento de la interfaz de
usuario, como una página, un cuadro de diálogo o un formulario de entrada de datos.
Los componentes son clases de .NET compiladas en ensamblados de .NET que:
definen la lógica de representación de la interfaz de usuario flexible;
controlan acciones del usuario;
se pueden anidar y reutilizar;
se pueden compartir y distribuir como bibliotecas de clases de Razor o paquetes de NuGet.
La clase del componente se escribe normalmente en forma de una página de marcado de Razor con una
extensión de archivo .razor. Se hace referencia a los componentes de Blazor como componentes de Razor. Razor
es una sintaxis para combinar marcado HTML con código de C# diseñada para aumentar la productividad del
desarrollador. Razor le permite cambiar entre marcado HTML y C# en el mismo archivo gracias a la
compatibilidad con IntelliSense. Razor Pages y MVC también usan Razor. A diferencia de Razor Pages y MVC,
que se compilan en torno a un modelo de solicitud y respuesta, los componentes se usan específicamente en la
composición y la lógica de la interfaz de usuario del lado cliente.
El siguiente marcado de Razor muestra un componente (Dialog.razor), que se puede anidar dentro de otro
componente:
<div>
<h1>@Title</h1>

@ChildContent

<button @onclick="@OnYes">Yes!</button>
</div>

@code {
[Parameter]
private string Title { get; set; }

[Parameter]
private RenderFragment ChildContent { get; set; }

private void OnYes()


{
Console.WriteLine("Write to the console in C#! 'Yes' button was selected.");
}
}

El contenido del cuerpo ( ChildContent ) y el título ( Title ) del cuadro de diálogo los proporciona el componente
que usa este componente en su interfaz de usuario. OnYes es un método de C# desencadenado por el evento
onclick del botón.

Blazor usa etiquetas HTML naturales para la composición de la interfaz de usuario. Los elementos HTML
especifican componentes y los atributos de la etiqueta pasan valores a las propiedades del componente.
En el ejemplo siguiente, el componente Index utiliza el componente Dialog . ChildContent y Title se
establecen mediante los atributos y el contenido del elemento <Dialog> .
Index.razor:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<Dialog Title="Blazor">
Do you want to <i>learn more</i> about Blazor?
</Dialog>

El cuadro de diálogo se representa cuando se accede al elemento principal (Index.razor) en un explorador:

Cuando este componente se usa en la aplicación, IntelliSense en Visual Studio y Visual Studio Code acelera el
desarrollo con la finalización de parámetros y sintaxis.
Los componentes se representan en una representación en memoria de la especificación Document Object
Model (DOM ) del explorador llamada árbol de representación, que se usa para actualizar la interfaz de usuario
de una manera eficaz y flexible.

Lado cliente de Blazor


El lado cliente de Blazor es un marco de trabajo de aplicación de una única página para la creación de
aplicaciones web interactivas en el lado cliente con. NET. El cliente de Blazor usa estándares web abiertos sin
complementos o transpilación de código y funciona en todos los exploradores web modernos, incluidos los
exploradores para dispositivos móviles.
La ejecución de código .NET dentro de exploradores web se consigue mediante WebAssembly (abreviado como
wasm). WebAssembly es un formato de código de bytes compacto optimizado para descargas rápidas y una
velocidad de ejecución máxima. WebAssembly es un estándar web abierto y se admite en exploradores web sin
complementos.
El código de WebAssembly puede acceder a toda la funcionalidad del explorador mediante la interoperabilidad
de JavaScript. El código de .NET que se ejecuta a través de WebAssembly en el explorador se ejecuta a su vez en
el espacio aislado de JavaScript del explorador con las protecciones que proporciona dicho espacio aislado
contra acciones malintencionadas en la máquina cliente.

Cuando se compila y ejecuta una aplicación del lado cliente de Blazor en un explorador:
Los archivos de código de C# y los archivos de Razor se compilan en ensamblados de .NET.
Los ensamblados y el entorno de ejecución de .NET se descargan en el explorador.
El lado cliente de Blazor arranca el entorno de ejecución .NET y lo configura para cargar los ensamblados de
la aplicación. El entorno de ejecución del lado cliente de Blazor emplea la interoperabilidad de JavaScript para
gestionar la manipulación de DOM y las llamadas API del explorador.
El tamaño de la aplicación publicada, su tamaño de carga, es un factor de rendimiento crítico para la facilidad de
uso de una aplicación. Una aplicación grande tarda un tiempo relativamente largo en descargarse en un
explorador, lo que repercute en la experiencia del usuario. El lado cliente de Blazor optimiza el tamaño de carga
para reducir los tiempos de descarga:
se ha quitado el código sin usar de la aplicación cuando se publica mediante el enlazador del lenguaje
intermedio (IL ).
Las respuestas HTTP se comprimen.
El entorno de ejecución .NET y los ensamblados se almacenan en caché en el explorador.

Lado servidor de Blazor


Blazor separa la lógica de representación de componentes del modo en que se aplican las actualizaciones de la
interfaz de usuario. El lado servidor de Blazor agrega compatibilidad con el hospedaje de componentes de Razor
en el servidor en una aplicación ASP.NET Core. Las actualizaciones de la interfaz de usuario se controlan
mediante una conexión de SignalR.
El entorno de ejecución controla el envío de eventos de la interfaz de usuario del explorador al servidor y,
después de ejecutar los componentes, vuelve a aplicar al explorador las actualizaciones de la interfaz de usuario
enviadas por el servidor.
La conexión utilizada por el lado servidor de Blazor para comunicarse con el explorador también se usa para
controlar las llamadas de interoperabilidad de JavaScript.

Interoperabilidad de JavaScript
En el caso de aplicaciones que necesitan bibliotecas de JavaScript de terceros y acceso a las API de explorador,
los componentes interoperan con JavaScript. Los componentes pueden usar cualquier biblioteca o API que
pueda utilizar JavaScript. El código de C# puede llamar a código JavaScript y, a su vez, el código JavaScript
puede llamar a código de C#. Para más información, consulte ASP.NET Core Blazor JavaScript interop.

Uso compartido de código y .NET Standard


Blazor implementa .NET Standard 2.0. .NET Standard es una especificación formal de las API de .NET comunes
entre las implementaciones de .NET. Las bibliotecas de clase .NET Standard pueden compartirse a través de
diferentes plataformas .NET, como Blazor, .NET Framework, .NET Core, Xamarin, Mono y Unity.
Las API que no pueden aplicarse dentro de un explorador web (por ejemplo, para acceder al sistema de archivos,
abrir un socket y ejecutar subprocesos) generan una excepción PlatformNotSupportedException.

Recursos adicionales
WebAssembly
ASP.NET Core Blazor hosting models
Guía de C#
Referencia de sintaxis de Razor para ASP.NET Core
HTML
Creación de la primera aplicación Blazor
17/06/2019 • 14 minutes to read • Edit Online

Por Daniel Roth y Luke Latham


En este tutorial se muestra cómo crear y modificar una aplicación de Blazor.
Siga las instrucciones del artículo Get started with ASP.NET Core Blazor para crear un proyecto de Blazor en este
tutorial.

Creación de componentes
1. Vaya a cada una de las tres páginas de la aplicación en la carpeta Pages: Home (Inicio), Counter (Contador)
y Fetch data (Recuperar datos). Estas páginas se implementan mediante los archivos de componente de
Razor Index.razor, Counter.razor y FetchData.razor.
2. En la página Contador, seleccione el botón Click me para aumentar el contador sin una actualización de
página. Aumentar un contador en una página web suele requerir la escritura de JavaScript, pero Blazor
proporciona una mejor manera de usar C#.
3. Examine la implementación del componente Counter en el archivo Counter.razor.
Pages/Counter.razor:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount++;
}
}

la interfaz de usuario del componente Counter se define mediante HTML. La lógica de la representación
dinámica (por ejemplo, bucles, instrucciones condicionales, expresiones) se agrega mediante una sintaxis de
C# insertada denominada Razor. El marcado HTML y la lógica de representación de C# se convierten en
una clase de componente en tiempo de compilación. El nombre de la clase de .NET generada coincide con el
nombre del archivo.
Los miembros de la clase de componente se definen en un bloque @code . En el bloque @code , se especifica
el estado del componente (propiedades, campos) y los métodos para el tratamiento de eventos o para
definir otra lógica del componente. Estos miembros se utilizan como parte de la lógica de representación
del componente y para el tratamiento de eventos.
Al seleccionarse el botón Click me:
Se llama al controlador onclick registrado del componente Counter (el método IncrementCount ).
El componente Counter regenera su árbol de representación.
El nuevo árbol de representación se compara con el anterior.
Únicamente se aplican modificaciones en Document Object Model (DOM ). Se actualiza el recuento
mostrado.
4. Modifique la lógica de C# del componente Counter para hacer que el recuento se incremente en dos en
lugar de uno.

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()


{
currentCount += 2;
}
}

5. Recompile y ejecute la aplicación para ver los cambios. Seleccione el botón Hacer clic aquí. El contador se
incrementa en dos.

Uso de componentes
Incluya un componente en otro componente mediante una sintaxis HTML.
1. Agregue el componente Counter al componente Index de la aplicación; para ello, agregue un elemento
<Counter /> al componente Index ( Index.razor).

Si usa Blazor para esta experiencia, hay un componente Survey Prompt (elemento <SurveyPrompt> ) en el
componente Index. Reemplace el elemento <SurveyPrompt> por el elemento <Counter> . Si usa una
aplicación de servidor de Blazor para esta experiencia, agregue el elemento <Counter> al componente
Index:
Pages/Index.razor:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<Counter />

2. Recompile y ejecute la aplicación. El componente Index tiene su propio contador.

Parámetros del componente


Los componentes también pueden tener parámetros. Los parámetros del componente se definen mediante
propiedades privadas en la clase de componentes decorada con [Parameter] . Use atributos para especificar
argumentos para un componente en el marcado.
1. Actualice el código de C# @code del componente:
Agregue una propiedad IncrementAmount decorada con el atributo [Parameter] .
Cambie el método IncrementCount para usar IncrementAmount al aumentar el valor de currentCount .
Pages/Counter.razor:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="@IncrementCount">Click me</button>

@code {
private int currentCount = 0;

[Parameter]
private int IncrementAmount { get; set; } = 1;

private void IncrementCount()


{
currentCount += IncrementAmount;
}
}

1. Especifique un parámetro IncrementAmount en el elemento <Counter> del componente Index mediante un


atributo. Establezca el valor para incrementar el contador en diez.
Pages/Index.razor:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<Counter IncrementAmount="10" />

2. Vuelva a cargar el componente Index. El contador se incrementa en diez cada vez que se selecciona el botón
Click me. El contador del componente Counter sigue incrementándose en uno.

Enrutamiento a los componentes


La directiva @page en la parte superior del archivo Counter.razor especifica que el componente Counter es un
punto de conexión de enrutamiento. El componente Counter controla las solicitudes enviadas a /counter . Sin la
directiva @page , el componente no controla las solicitudes enrutadas, pero otros componentes aún pueden usar el
componente.

Inserción de dependencias
Los servicios registrados en el contenedor de servicios de la aplicación están disponibles para los componentes
mediante una inserción de dependencia (DI). Inserte servicios en un componente mediante la directiva @inject .
Examine las directivas del componente FetchData.
Si trabaja con la aplicación de servidor de Blazor, el servicio WeatherForecastService se registra como singleton, de
modo que una instancia del servicio está disponible en toda la aplicación. La directiva @inject se usa para insertar
la instancia del servicio WeatherForecastService en el componente.
Pages/FetchData.razor:

@page "/fetchdata"
@using WebApplication1.App.Services
@inject WeatherForecastService ForecastService

El componente FetchData usa el servicio insertado, como ForecastService , para recuperar una matriz de objetos
WeatherForecast :

@code {
private WeatherForecast[] forecasts;

protected override async Task OnInitAsync()


{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}

Si trabaja con la aplicación cliente de Blazor, se inserta HttpClient para obtener datos de previsión del tiempo del
archivo weather.json de la carpeta wwwroot/sample-data:
Pages/FetchData.razor:

@inject HttpClient Http

...

protected override async Task OnInitAsync()


{
forecasts =
await Http.GetJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}

Se usa un bucle @foreach para representar cada instancia de previsión como una fila de la tabla de datos
meteorológicos:

<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>

Creación de una lista de tareas pendientes


Agregue un nuevo componente a la aplicación que implemente una simple lista de tareas pendientes.
1. Agregue un archivo vacío denominado Todo.razor a la aplicación en la carpeta Pages:
2. Proporcione el marcado inicial para el componente:

@page "/todo"

<h1>Todo</h1>

3. Agregue el componente Todo a la barra de navegación.


El componente NavMenu (Shared/NavMenu.razor) se usa en el diseño de la aplicación. Los diseños son
componentes que le permiten impedir la duplicación de contenido en la aplicación. Para obtener más
información, vea ASP.NET Core Blazor layouts.
Agregue un elemento <NavLink> a la página Todo mediante la adición del siguiente marcado de elementos
de lista debajo de los elementos de lista existentes en el archivo Shared/NavMenu.razor:

<li class="nav-item px-3">


<NavLink class="nav-link" href="todo">
<span class="oi oi-list-rich" aria-hidden="true"></span> Todo
</NavLink>
</li>

4. Recompile y ejecute la aplicación. Visite la nueva página Todo para confirmar que el vínculo al componente
Todo funcione.
5. Agregue un archivo TodoItem.cs a la raíz del proyecto para contener una clase que represente un elemento
de la lista de tareas. Use el siguiente código de C# para la clase TodoItem :

public class TodoItem


{
public string Title { get; set; }
public bool IsDone { get; set; }
}

6. Vuelva al componente Todo (Pages/Todo.razor):


Agregue un campo a los elementos de tareas pendientes en un bloque @code . El componente Todo
utiliza este campo para mantener el estado de la lista de tareas pendientes.
Agregue el marcado de la lista no ordenada y un bucle foreach para que cada elemento de la lista se
represente en un elemento de la lista de tareas pendientes.

@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
}
7. Para agregar elementos de tareas pendientes a la lista, la aplicación requiere elementos de la interfaz de
usuario. Agregue una entrada de texto y un botón debajo de la lista:

@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" />


<button>Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
}

8. Recompile y ejecute la aplicación. Cuando se selecciona el botón Add todo (Agregar tarea pendiente), no
ocurre nada porque no hay ningún controlador de eventos conectado al botón.
9. Agregue un método AddTodo al componente Todo y regístrelo para hacer clic en los botones mediante el
atributo @onclick :

<input placeholder="Something todo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();

private void AddTodo()


{
// Todo: Add the todo
}
}

El método AddTodo de C# se llama cuando se selecciona el botón.


10. Para obtener el título del nuevo elemento de tarea pendiente, agregue un campo de cadena newTodo y
enlácelo al valor de la entrada de texto mediante el atributo bind :

private IList<TodoItem> todos = new List<TodoItem>();


private string newTodo;

<input placeholder="Something todo" @bind="@newTodo" />

11. Actualice el método AddTodo para agregar el TodoItem con el título especificado a la lista. Borre el valor de
la entrada de texto mediante el establecimiento de newTodo en una cadena vacía:
@page "/todo"

<h1>Todo</h1>

<ul>
@foreach (var todo in todos)
{
<li>@todo.Title</li>
}
</ul>

<input placeholder="Something todo" @bind="@newTodo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

12. Recompile y ejecute la aplicación. Agregue algunos elementos de tareas pendientes a la lista de tareas
pendientes para probar el nuevo código.
13. Se puede hacer que el texto de título de cada elemento de tarea pendiente sea editable y una casilla puede
ayudar al usuario a realizar un seguimiento de los elementos completados. Agregue una entrada de casilla a
cada elemento de tarea pendiente y enlace su valor a la propiedad IsDone . Cambie @todo.Title a un
elemento <input> enlazado a @todo.Title :

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>

14. Para comprobar que estos valores están enlazados, actualice el encabezado <h1> para mostrar un recuento
del número de elementos de la lista de tareas pendientes que no se han completado ( IsDone es false ).

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

15. El componente Todo completado (Pages/Todo.razor):


@page "/todo"

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="@todo.IsDone" />
<input @bind="@todo.Title" />
</li>
}
</ul>

<input placeholder="Something todo" @bind="@newTodo" />


<button @onclick="@AddTodo">Add todo</button>

@code {
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;

private void AddTodo()


{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}

16. Recompile y ejecute la aplicación. Agregue elementos de tarea pendiente para probar el nuevo código.

Publicar e implementar la aplicación


Para publicar la aplicación, consulte Hospedaje e implementación de ASP.NET Core Blazor.
Autenticación y autorización de ASP.NET Core Blazor
02/07/2019 • 20 minutes to read • Edit Online

Por Steve Sanderson


ASP.NET Core admite la configuración y administración de seguridad en las aplicaciones Blazor.
Los escenarios de seguridad difieren entre las aplicaciones del lado servidor y del lado cliente de Blazor. Debido a
que las aplicaciones del lado servidor de Blazor se ejecutan en el servidor, las comprobaciones de autorización
pueden determinar:
Las opciones de la interfaz de usuario presentadas a un usuario (por ejemplo, qué entradas de menú están
disponibles para el usuario).
Las reglas de acceso para las áreas de la aplicación y los componentes.
Las aplicaciones Blazor del lado cliente que se ejecutan en el cliente. La autorización solo se utiliza para determinar
qué opciones de la interfaz de usuario se van a mostrar. Dado que las comprobaciones del lado cliente pueden
modificarse u omitirse por el usuario, una aplicación Blazor del lado cliente no puede aplicar las reglas de acceso de
autorización.

Autenticación
Blazor utiliza los mecanismos de autenticación de ASP.NET Core existentes para establecer la identidad del usuario.
El mecanismo exacto depende de cómo esté hospedada la aplicación Blazor, del lado servidor o del lado cliente.
Autenticación del lado servidor de Blazor
Las aplicaciones del lado servidor de Blazor operan sobre una conexión en tiempo real que se crearon con SignalR.
La autenticación en aplicaciones basadas en SignalR se controla cuando se establece la conexión. La autenticación
se puede basar en una cookie o en cualquier otro token de portador.
La plantilla de proyecto del lado servidor de Blazor puede configurar la autenticación cuando se crea el proyecto.
Visual Studio
Visual Studio Code
Siga las instrucciones de Visual Studio en el artículo Get started with ASP.NET Core Blazor para crear un nuevo
proyecto en el lado servidor de Blazor con un mecanismo de autenticación.
Después de elegir la plantilla de Blazor (lado servidor) en el cuadro de diálogo Crear una aplicación web
ASP.NET Core, seleccione Cambiar en Autenticación.
Se abre un cuadro de diálogo para ofrecer el mismo conjunto de mecanismos de autenticación disponibles para
otros proyectos ASP.NET Core:
Sin autenticación
Cuentas de usuario individuales. Las cuentas de usuario se pueden almacenar:
Dentro de la aplicación mediante el sistema de identidad de ASP.NET Core.
Con Azure AD B2C
Cuentas profesionales o educativas
Autenticación de Windows
Autenticación del lado cliente de Blazor
En las aplicaciones Blazor del lado cliente, las comprobaciones de autenticación pueden omitirse porque los
usuarios pueden modificar todos los códigos del lado cliente. Lo mismo se aplica a todas las tecnologías de
aplicaciones del lado cliente, incluidas las plataformas JavaScript SPA o las aplicaciones nativas para cualquier
sistema operativo.
En las secciones siguientes se trata la implementación de un servicio AuthenticationStateProvider personalizado
para aplicaciones Blazor del lado cliente.

Servicio AuthenticationStateProvider
Las aplicaciones Blazor del lado servidor incluyen un servicio AuthenticationStateProvider integrado que obtiene
los datos de estado de autenticación de HttpContext.User de ASP.NET Core. Así es como el estado de
autenticación se integra con los mecanismos de autenticación existentes en el lado servidor de ASP.NET Core.
es el servicio subyacente utilizado por el componente AuthorizeView y el
AuthenticationStateProvider
componente CascadingAuthenticationState para obtener el estado de autenticación.
Por lo general, no se utiliza AuthenticationStateProvider directamente. Use los enfoques del componente
AuthorizeView o Task descritos más adelante en este artículo. El principal inconveniente de utilizar
AuthenticationStateProvider directamente es que el componente no se notifica de manera automática si cambia el
estado de autenticación subyacente de los datos.
El servicio AuthenticationStateProvider puede proporcionar los datos ClaimsPrincipal del usuario actual, como se
muestra en el ejemplo siguiente:

@page "/"
@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="@LogUsername">Write user info to console</button>

@code {
private async Task LogUsername()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

if (user.Identity.IsAuthenticated)
{
Console.WriteLine($"{user.Identity.Name} is authenticated.");
}
else
{
Console.WriteLine("The user is NOT authenticated.");
}
}
}

Si user.Identity.IsAuthenticated es true y porque el usuario es ClaimsPrincipal, se pueden enumerar las


notificaciones y evaluar la pertenencia a roles.
Para más información sobre la inserción de dependencias (DI) y servicios, consulte ASP.NET Core Blazor
dependency injection y Inserción de dependencias en ASP.NET Core.

Implementación de un componente AuthenticationStateProvider


personalizado
Si está creando una aplicación Blazor del lado cliente o si la especificación de la aplicación requiere absolutamente
un proveedor personalizado, implemente un proveedor y reemplace GetAuthenticationStateAsync :
class CustomAuthStateProvider : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "mrfibuli"),
}, "Fake authentication type");

var user = new ClaimsPrincipal(identity);

return Task.FromResult(new AuthenticationState(user));


}
}

El servicio CustomAuthStateProvider se registra en Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
}

Mediante CustomAuthStateProvider , todos los usuarios se autentican con el nombre de usuario mrfibuli .

Exposición del estado de autenticación como un parámetro en cascada


Si se requieren los datos de estado de autenticación para la lógica de procedimiento, como cuando se realiza una
acción desencadenada por el usuario, obtenga los datos de estado de autenticación mediante la definición de un
parámetro en cascada del tipo Task<AuthenticationState> :

@page "/"

<button @onclick="@LogUsername">Log username</button>

@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

private async Task LogUsername()


{
var authState = await authenticationStateTask;
var user = authState.User;

if (user.Identity.IsAuthenticated)
{
Console.WriteLine($"{user.Identity.Name} is authenticated.");
}
else
{
Console.WriteLine("The user is NOT authenticated.");
}
}
}

Si user.Identity.IsAuthenticated es true , se pueden enumerar las notificaciones y evaluar la pertenencia a roles.


Configure el parámetro Task<AuthenticationState> en cascada mediante el componente
CascadingAuthenticationState :
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Startup).Assembly">
<NotFoundContent>
<h1>Sorry</h1>
<p>Sorry, there's nothing at this address.</p>
</NotFoundContent>
</Router>
</CascadingAuthenticationState>

Autorización
Cuando un usuario está autenticado, se aplican las reglas de autorización para controlar qué puede hacer el
usuario.
Por lo general, se concede o deniega el acceso en función de si:
El usuario está autenticado (ha iniciado sesión).
El usuario está en un rol.
El usuario tiene una notificación.
Una directiva se cumple.
Cada uno de estos conceptos es el mismo que en una aplicación ASP.NET Core MVC o Razor Pages. Para más
información sobre la seguridad de ASP.NET Core, consulte los artículos que se encuentran en ASP.NET Core
Security and Identity (Identidad y seguridad de ASP.NET Core).

Componente AuthorizeView
El componente AuthorizeView selectivamente muestra la interfaz de usuario dependiendo de si el usuario está
autorizado para verlo. Este enfoque es útil cuando solo necesite mostrar datos del usuario y no es necesario usar la
identidad del usuario en la lógica de procedimientos.
El componente expone una variable context del tipo AuthenticationState , que puede utilizar para acceder a la
información sobre el usuario que ha iniciado sesión:

<AuthorizeView>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</AuthorizeView>

También puede proporcionar contenido diferente para mostrar si el usuario no está autenticado:

<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>

El contenido de <Authorized> y <NotAuthorized> puede incluir elementos arbitrarios, como otros componentes
interactivos.
Las condiciones de autorización, como los roles o directivas que controlan las opciones o el acceso a la interfaz de
usuario, se tratan en la sección Autorización.
Si no se especifican las condiciones de la autorización, AuthorizeView usa una directiva predeterminada y trata:
A los usuarios autenticados (con sesión iniciada) como autorizados.
A los usuarios no autenticados (sin sesión no iniciada) como no autorizados.
Autorización basada en roles y en directivas
El componente AuthorizeView admite la autorización basada en roles o basada en directivas.
Para la autorización basada en roles, utilice el parámetro Roles :

<AuthorizeView Roles="admin, superuser">


<p>You can only see this if you're an admin or superuser.</p>
</AuthorizeView>

Para más información, consulte Autorización basada en roles en ASP.NET Core.


Para la autorización basada en directivas, utilice el parámetro Policy :

<AuthorizeView Policy="content-editor">
<p>You can only see this if you satisfy the "content-editor" policy.</p>
</AuthorizeView>

La autorización basada en notificaciones es un caso especial de autorización basada en directivas. Por ejemplo,
puede definir una directiva que requiere que los usuarios tengan una notificación determinada. Para más
información, consulte Autorización basada en directivas en ASP.NET Core.
Estas API se pueden usar en aplicaciones Blazor del lado cliente o del lado servidor.
Si no se especifica Roles ni Policy , AuthorizeView usa la directiva predeterminada.
Contenido que se muestra durante la autenticación asincrónica
Blazor permite que el estado de autenticación se determine asincrónicamente. El escenario principal para este
enfoque se encuentra en las aplicaciones Blazor en el lado cliente que realice una solicitud a un punto de conexión
externo para la autenticación.
Mientras la autenticación está en curso, AuthorizeView no muestra ningún contenido de forma predeterminada.
Para mostrar el contenido mientras tiene lugar la autenticación, use el elemento <Authorizing> :

<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</Authorized>
<Authorizing>
<h1>Authentication in progress</h1>
<p>You can only see this content while authentication is in progress.</p>
</Authorizing>
</AuthorizeView>

Este enfoque no se aplica normalmente a las aplicaciones Blazor del lado servidor. Las aplicaciones Blazor del lado
servidor conocen el estado de autenticación tan pronto como se establece dicho estado. El contenido Authorizing
puede proporcionarse en el componente AuthorizeView de una aplicación Blazor del lado servidor, pero el
contenido nunca se muestra.
Atributo [Authorize]
Al igual que una aplicación puede usar [Authorize] con un controlador de MVC o la página de Razor, [Authorize]
también se puede usar con los componentes de Razor:

@page "/"
@attribute [Authorize]

You can only see this if you're signed in.

IMPORTANT
Utilice únicamente [Authorize] en componentes @page a los que se llega a través del enrutador de Blazor. La autorización
solo se realiza como un aspecto del enrutamiento y no para los componentes secundarios representados dentro de una
página. Para autorizar la presentación de partes concretas dentro de una página, use AuthorizeView en su lugar.

Es posible que deba agregar @using Microsoft.AspNetCore.Authorization al componente o al archivo _Imports.razor


para que se compile el componente.
El atributo [Authorize] admite también la autorización basada en roles o basada en directivas. Para la autorización
basada en roles, utilice el parámetro Roles :

@page "/"
@attribute [Authorize(Roles = "admin, superuser")]

<p>You can only see this if you're in the 'admin' or 'superuser' role.</p>

Para la autorización basada en directivas, utilice el parámetro Policy :

@page "/"
@attribute [Authorize(Policy = "content-editor")]

<p>You can only see this if you satisfy the 'content-editor' policy.</p>

Si no se especifica Roles ni Policy , [Authorize] usa la directiva predeterminada, que consiste en tratar:
A los usuarios autenticados (con sesión iniciada) como autorizados.
A los usuarios no autenticados (sin sesión no iniciada) como no autorizados.

Personalización del contenido no autorizado con el componente de


enrutador
El componente Router permite que la aplicación para especificar el contenido personalizado si:
No se encuentra el contenido.
El usuario produce un error en la condición [Authorize] aplicada al componente. El atributo [Authorize] se
trata en la sección Atributo [Authorize].
La autenticación asincrónica está en curso.
En la plantilla de proyecto predeterminada del lado servidor de Blazor, el archivo App.razor muestra cómo
configurar el contenido personalizado:
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Startup).Assembly">
<NotFoundContent>
<h1>Sorry</h1>
<p>Sorry, there's nothing at this address.</p>
</NotFoundContent>
<NotAuthorizedContent>
<h1>Sorry</h1>
<p>You're not authorized to reach this page.</p>
<p>You may need to log in as a different user.</p>
</NotAuthorizedContent>
<AuthorizingContent>
<h1>Authentication in progress</h1>
<p>Only visible while authentication is in progress.</p>
</AuthorizingContent>
</Router>
</CascadingAuthenticationState>

El contenido de <NotFoundContent> , <NotAuthorizedContent> y <AuthorizingContent> puede incluir elementos


arbitrarios, como otros componentes interactivos.
Si <NotAuthorizedContent> no se especifica, el enrutador utiliza el siguiente mensaje de reserva:

Not authorized.

Notificación sobre los cambios de estado de autenticación


Si la aplicación determina que los datos de estado de autenticación subyacentes han cambiado (por ejemplo,
porque el usuario ha cerrado sesión o porque otro usuario ha cambiado sus roles), un AuthenticationStateProvider
personalizado puede opcionalmente invocar el método NotifyAuthenticationStateChanged en la clase base
AuthenticationStateProvider . Esto notifica a los consumidores de los datos de estado de autenticación (por
ejemplo, AuthorizeView ) para que los vuelvan a procesar utilizando los nuevos datos.

Lógica de procedimientos
Si se requiere que la aplicación compruebe las reglas de autorización como parte de la lógica de procedimiento,
utilice un parámetro en cascada del tipo Task<AuthenticationState> para obtener el ClaimsPrincipal del usuario.
Task<AuthenticationState> se puede combinar con otros servicios, como IAuthorizationService , para evaluar las
directivas.
@inject IAuthorizationService AuthorizationService

<button @onclick="@DoSomething">Do something important</button>

@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

private async Task DoSomething()


{
var user = (await authenticationStateTask).User;

if (user.Identity.IsAuthenticated)
{
// Perform an action only available to authenticated (signed-in) users.
}

if (user.IsInRole("admin"))
{
// Perform an action only available to users in the 'admin' role.
}

if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))


.Succeeded)
{
// Perform an action only available to users satisfying the
// 'content-editor' policy.
}
}
}

Autorización en las aplicaciones Blazor del lado cliente


En las aplicaciones Blazor del lado cliente, las comprobaciones de autenticación pueden omitirse porque los
usuarios pueden modificar todos los códigos del lado cliente. Lo mismo se aplica a todas las tecnologías de
aplicaciones del lado cliente, incluidas las plataformas JavaScript SPA o las aplicaciones nativas para cualquier
sistema operativo.
Realice siempre las comprobaciones de autorización en el servidor dentro de cualquier punto de
conexión de la API al que acceda su aplicación del lado cliente.

Solución de errores
Errores comunes:
La autorización requiere un parámetro en casada de tipo Task<AuthenticationState>. Considere el
uso de CascadingAuthenticationState para proporcionarla.
Se recibe el valor null para authenticationStateTask

Es probable que el proyecto no se haya creado mediante una plantilla del lado servidor de Blazor con la
autenticación habilitada. Encapsule un <CascadingAuthenticationState> alrededor de alguna parte del árbol de la
interfaz, por ejemplo, en App.razor de la siguiente manera:

<CascadingAuthenticationState>
<Router AppAssembly="typeof(Startup).Assembly">
...
</Router>
</CascadingAuthenticationState>
El CascadingAuthenticationState proporciona el parámetro en cascada Task<AuthenticationState> , que a su vez
recibe el servicio DI AuthenticationStateProvider subyacente.

Recursos adicionales
Introducción a la seguridad de ASP.NET Core
Configurar la autenticación de Windows en ASP.NET Core
Hospedaje e implementación de ASP.NET Core Blazor
19/06/2019 • 2 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Publicar la aplicación
Las aplicaciones se publican para implementación en la configuración de versión.
Visual Studio
Visual Studio Code y CLI de .NET Core
1. Seleccione Compilar > Publicar {aplicación} en la barra de navegación.
2. Seleccione el destino de publicación. Para publicar localmente, seleccione Carpeta.
3. Acepte la ubicación predeterminada del campo Elegir una carpeta o especifique una ubicación diferente.
Seleccione el botón Publicar.
Al publicar la aplicación se desencadena una restauración de las dependencias del proyecto y se compila el
proyecto antes de crear los recursos para la implementación. Como parte del proceso de compilación, se quitan los
ensamblados y métodos que no se usan para reducir los tiempos de carga y el tamaño de descarga de la aplicación.
Una aplicación cliente de Blazor se publica en la carpeta /bin/Release/{RED DE DESTINO }/publish/{NOMBRE DE
ENSAMBLADO }/dist. Una aplicación de servidor de Blazor se publica en la carpeta /bin/Release/{TARGET
FRAMEWORK }/publish.
Los recursos de la carpeta se implementan en el servidor web. La implementación puede ser un proceso manual o
automatizado, en función de las herramientas de desarrollo que se usen.

Implementación
Para una guía sobre la implementación, consulte los temas siguientes:
Hospedaje e implementación de ASP.NET Core Blazor del lado cliente
Hospedaje e implementación de ASP.NET Core Blazor del lado servidor

Hospedaje sin servidor de Blazor con Azure Storage


Las aplicaciones de cliente de Blazor pueden obtenerse de Azure Storage como contenido estático directamente
desde un contenedor de almacenamiento.
Para más información, consulte Hospedaje e implementación de ASP.NET Core Blazor del lado cliente
(implementación independiente): Azure Storage.
Hospedaje e implementación de ASP.NET Core Blazor
del lado cliente
03/07/2019 • 24 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Valores de configuración de host


Las aplicaciones de Blazor que usan el modelo de hospedaje del lado cliente pueden aceptar los siguientes valores
de configuración de host como argumentos de línea de comandos en tiempo de ejecución en el entorno de
desarrollo.
Raíz del contenido
El argumento --contentroot establece la ruta de acceso absoluta al directorio que incluye los archivos de
contenido de la aplicación. En los ejemplos siguientes, /content-root-path es la ruta de acceso raíz del contenido
de la aplicación.
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --contentroot=/content-root-path

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--contentroot=/content-root-path"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--contentroot=/content-root-path

Ruta de acceso base


El argumento --pathbase establece la ruta de acceso base de la aplicación para una aplicación que se ejecuta
localmente con una ruta de acceso virtual que no es raíz (el valor href de la etiqueta <base> se establece en una
ruta de acceso que no sea / para ensayo y producción). En los ejemplos siguientes, /virtual-path es la ruta de
acceso base de la aplicación. Para obtener más información, vea la sección Ruta de acceso base de la aplicación.

IMPORTANT
A diferencia de la ruta de acceso proporcionada al valor href de la etiqueta <base> , no incluya una barra diagonal final (
/ ) al pasar el valor del argumento --pathbase . Si se proporciona la ruta de acceso base de la aplicación en la etiqueta
<base> como <base href="/CoolApp/"> (se incluye una barra diagonal final), se pasa el valor del argumento de línea de
comandos como --pathbase=/CoolApp (sin barra diagonal final).
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --pathbase=/virtual-path

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--pathbase=/virtual-path"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--pathbase=/virtual-path

Direcciones URL
El argumento --urls establece las direcciones IP o las direcciones de host con los puertos y protocolos en los que
escuchar las solicitudes.
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --urls=http://127.0.0.1:0

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--urls=http://127.0.0.1:0"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--urls=http://127.0.0.1:0

Implementación
Con el modelo de hospedaje del lado cliente:
La aplicación Blazor, sus dependencias y el entorno de ejecución de .NET se descargan en el explorador.
La aplicación se ejecuta directamente en el subproceso de interfaz de usuario del explorador. Se puede seguir
cualquiera de las estrategias siguientes:
Una aplicación ASP.NET Core proporciona la aplicación Blazor. Esta estrategia se trata en la sección
Implementación hospedada con ASP.NET Core.
La aplicación Blazor se coloca en un servicio o servidor web de hospedaje estático, donde no se usa .NET
para proporcionar la aplicación Blazor. Esta estrategia se trata en la sección Implementación
independiente.

Configurar el enlazador
Blazor realiza la vinculación de lenguaje intermedio (IL ) en cada compilación para quitar el IL innecesario de los
ensamblados de salida. La vinculación de ensamblados puede controlarse en la compilación. Para más información,
consulte Configuración del enlazador para ASP.NET Core Blazor.

Reescritura de las URL para conseguir un enrutamiento correcto


Enrutar las solicitudes para los componentes de la página en una aplicación del lado cliente no es tan sencillo como
enrutar las solicitudes a una aplicación hospedada del lado servidor. Imagine que tiene una aplicación del lado
cliente con dos páginas:
_Main.razor: se carga en la raíz de la aplicación y contiene un vínculo a la página de información ( href="About"
).
_About.Razor: página Acerca de.
Cuando se solicita el documento predeterminado de la aplicación mediante la barra de direcciones del explorador
(por ejemplo, https://www.contoso.com/ ):
1. El explorador realiza una solicitud.
2. Se devuelve la página predeterminada, que suele ser index.html.
3. index.html arranca la aplicación.
4. Se carga el enrutador de Blazor y se muestra la página principal de Razor (Main.razor).
En la página principal, al seleccionar el vínculo a la página de información, se carga la página de información.
Seleccionar el vínculo a la página de información funciona en el cliente porque el enrutador de Blazor impide que el
explorador realice una solicitud en Internet a www.contoso.com sobre About y presenta la propia página de
información. Todas las solicitudes de páginas internas dentro de la aplicación del lado cliente funcionan del mismo
modo: Las solicitudes no desencadenan solicitudes basadas en el explorador a recursos hospedados en el servidor
en Internet. El enrutador controla las solicitudes de forma interna.
Si se realiza una solicitud mediante la barra de direcciones del explorador para www.contoso.com/About , se produce
un error. Este recurso no existe en el host de Internet de la aplicación, por lo que se devuelve una respuesta 404 No
encontrado.
Dado que los exploradores realizan solicitudes a hosts basados en Internet para las páginas del lado cliente, los
servidores web y los servicios de hospedaje deben reescribir todas las solicitudes de recursos que no estén
físicamente en el servidor a la página index.html. Cuando se devuelve index.html, el enrutador de la aplicación del
lado cliente se hace cargo y responde con el recurso correcto.

Ruta de acceso base de la aplicación


La ruta de acceso base de la aplicación es la ruta de acceso raíz de la aplicación virtual en el servidor. Por ejemplo, a
una aplicación que reside en el servidor de Contoso, en una carpeta virtual de /CoolApp/ , se accede desde
https://www.contoso.com/CoolApp ; su ruta de acceso base virtual es /CoolApp/ . Al establecer la ruta de acceso base
de la aplicación en la ruta de acceso virtual ( <base href="/CoolApp/"> ), la aplicación sabe dónde reside virtualmente
en el servidor. La aplicación puede usar la ruta de acceso base de la aplicación para construir direcciones URL
relativas a la raíz de la aplicación desde un componente que no se encuentre en el directorio raíz. Esto permite a los
componentes que existen en diferentes niveles de la estructura de directorios compilar vínculos a otros recursos en
ubicaciones de toda la aplicación. La ruta de acceso base de la aplicación también se usa para interceptar clics en
hipervínculos en los que el destino href del vínculo está dentro del espacio de URI de la ruta de acceso base de la
aplicación y es el enrutador de Blazor quien controla la navegación interna.
En muchos escenarios de hospedaje, la ruta de acceso virtual del servidor a la aplicación es la raíz de la aplicación.
En estos casos, la ruta de acceso base de la aplicación es una barra diagonal ( <base href="/" /> ), que es la
configuración predeterminada para una aplicación. En otros escenarios de hospedaje, como las subaplicaciones o
los directorios virtuales de IIS y GitHub Pages, la ruta de acceso base de la aplicación debe establecerse en la ruta
de acceso virtual del servidor a la aplicación. Para establecer la ruta de acceso base de la aplicación, actualice la
etiqueta <base> dentro de los elementos de etiqueta <head> del archivo wwwroot/index.HTML. Establezca el valor
del atributo href en /virtual-path/ (la barra diagonal final es necesaria), donde /virtual-path/ es la ruta de
acceso raíz de la aplicación virtual completa en el servidor para la aplicación. En el ejemplo anterior, se establece la
ruta de acceso virtual en /CoolApp/ : <base href="/CoolApp/"> .
En el caso de una aplicación con una ruta de acceso virtual que no es raíz configurada (por ejemplo,
<base href="/CoolApp/"> ), la aplicación no puede encontrar sus recursos cuando se ejecuta de forma local. Para
solucionar este problema durante la fase de desarrollo y pruebas local, puede proporcionar un argumento de ruta
de acceso base que coincida con el valor href de la etiqueta <base> en tiempo de ejecución.
Para pasar el argumento de ruta de acceso base con la ruta de acceso raíz ( / ) al ejecutar la aplicación de forma
local, ejecute el comando dotnet run desde el directorio de la aplicación con la opción --pathbase :

dotnet run --pathbase=/{Virtual Path (no trailing slash)}

Para una aplicación con una ruta de acceso virtual base de /CoolApp/ ( <base href="/CoolApp/"> ), el comando es el
siguiente:

dotnet run --pathbase=/CoolApp

La aplicación responde de forma local en http://localhost:port/CoolApp .


Para más información, vea la sección sobre el valor de configuración de host de la ruta de acceso base.
Si una aplicación usa el modelo de hospedaje del lado cliente (basado en la plantilla de proyecto de Blazor; la
plantilla blazor al usar el comando dotnet new ) y se hospeda como una subaplicación de IIS en una aplicación
ASP.NET Core, es importante deshabilitar el controlador del módulo de ASP.NET Core heredado o asegurarse de
que la subaplicación no hereda la sección <handlers> de la aplicación raíz (principal) en el archivo web.config.
Para quitar el controlador del archivo web.config publicado de la aplicación, agregue una sección <handlers> al
archivo:

<handlers>
<remove name="aspNetCore" />
</handlers>

Como alternativa, deshabilite la herencia de la sección <system.webServer> de la aplicación raíz (principal) mediante
un elemento <location> con inheritInChildApplications establecido en false :
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" ... />
</handlers>
<aspNetCore ... />
</system.webServer>
</location>
</configuration>

Además de configurarse la ruta de acceso base de la aplicación, se quita el controlador o se deshabilita la herencia,
como se describe en esta sección. Establezca la ruta de acceso base de la aplicación en el archivo index.html de la
aplicación en el alias de IIS que ha usado al configurar la subaplicación en IIS.

Implementación hospedada con ASP.NET Core


Una implementación hospedada proporciona la aplicación Blazor del lado cliente a los exploradores desde una
aplicación ASP.NET Core que se ejecuta en un servidor.
La aplicación Blazor se incluye con la aplicación ASP.NET Core en la salida publicada para que ambas se
implementen juntas. Se requiere un servidor web que pueda hospedar una aplicación ASP.NET Core. En el caso de
una implementación hospedada, Visual Studio incluye la plantilla de proyecto de Blazor (hospedada en ASP.NET
Core) (la plantilla blazorhosted al usar el comando dotnet new ).
Para obtener más información sobre la implementación y el hospedaje de aplicaciones de ASP.NET Core, consulte
Hospedaje e implementación de ASP.NET Core.
Para obtener información sobre cómo implementar en Azure App Service, vea Publicar una aplicación de ASP.NET
Core en Azure con Visual Studio.

Implementación independiente
Una implementación independiente proporciona la aplicación Blazor del lado cliente como un conjunto de archivos
estáticos que los clientes solicitan directamente. Cualquier servidor de archivos estático es capaz de servir a la
aplicación Blazor.
Los activos de implementación independientes se publican en la carpeta /bin/Release/{RED DE
DESTINO }/publish/{NOMBRE DE ENSAMBLADO }/dist.
IIS
IIS es un servidor de archivos estáticos compatible con las aplicaciones de Blazor. Para configurar IIS para
hospedar Blazor, vea Build a Static Website on IIS (Compilación de un sitio web estático en IIS ).
Los recursos publicados se crean en la carpeta /bin/Release/{TARGET FRAMEWORK }/publish. Hospede el
contenido de la carpeta publish en el servidor web o el servicio de hospedaje.
web.config
Cuando se publica un proyecto de Blazor, se crea un archivo web.config con la siguiente configuración de IIS:
Se establecen los tipos MIME de las siguientes extensiones de archivo:
.dll – application/octet-stream
.json – application/json
.wasm – application/wasm
.woff – application/font-woff
.woff2 – application/font-woff
Se habilita la compresión HTTP de los siguientes tipos MIME:
application/octet-stream
application/wasm
Se establecen reglas del módulo URL Rewrite:
Proporcionar el subdirectorio donde residen los recursos estáticos de la aplicación ( {ASSEMBLY
NAME }/dist/{PATH REQUESTED } ).
Crear el enrutamiento de reserva de SPA para que las solicitudes de recursos que no sean archivos se
redirijan al documento predeterminado de la aplicación en su carpeta de recursos estáticos ( {ASSEMBLY
NAME }/dist/index.html).
Instalación del módulo URL Rewrite
El módulo URL Rewrite es necesario para reescribir las URL. El módulo no se instala de forma predeterminada y
no está disponible para instalar como una característica de servicio de rol del servidor web (IIS ). El módulo se debe
descargar desde el sitio web de IIS. Use el instalador de plataforma web para instalar el módulo:
1. De forma local, vaya a la página de descargas del módulo URL Rewrite. En el caso de la versión en inglés,
seleccione WebPI para descargar el instalador de WebPI. En el caso de otros idiomas, seleccione la arquitectura
adecuada del servidor (x86/x64) para descargar el instalador.
2. Copie el instalador en el servidor. Ejecute el instalador. Haga clic en el botón Instalar y acepte los términos de
licencia. No es necesario reiniciar el servidor al finalizar la instalación.
Configuración del sitio web
Configure la ruta de acceso física del sitio web a la carpeta de la aplicación. La carpeta contiene:
El archivo web.config que usa IIS para configurar el sitio web, incluidas las reglas de redireccionamiento y los
tipos de contenido de archivos necesarios.
La carpeta de recursos estáticos de la aplicación.
Solución de problemas
Si se recibe un error 500 Error interno del servidor y el administrador de IIS produce errores al intentar acceder a la
configuración del sitio web, confirme que el módulo URL Rewrite está instalado. Si no lo está, IIS no puede analizar
el archivo web.config. Esto impide que el Administrador de IIS cargue la configuración del sitio web y que el sitio
web proporcione los archivos estáticos de Blazor.
Para obtener más información sobre cómo solucionar problemas de las implementaciones en IIS, vea Solución de
problemas de ASP.NET Core en IIS.
Almacenamiento de Azure
El hospedaje de archivos estáticos de Azure Storage permite el hospedaje de aplicaciones Blazor sin servidor. Se
admiten nombres de dominio personalizados, Azure Content Delivery Network (CDN ) y HTTPS.
Cuando el servicio de blob está habilitado para el hospedaje de sitios web estáticos en una cuenta de
almacenamiento:
Establece el nombre de documento de índice en index.html .
Establece la ruta de acceso del documento de error en index.html . Los componentes Razor y otros puntos
de conexión que no son de archivo no residen en las rutas de acceso físicas del contenido estático almacenado
por el servicio de blob. Cuando se recibe una solicitud de uno de estos recursos que debe controlar el enrutador
de Blazor, el error 404: no encontrado generado por el servicio de blob enruta la solicitud a la ruta de acceso
del documento de error. Se devuelve el blob index.html y el enrutador de Blazor carga y procesa la ruta de
acceso.
Para más información, consulte Hospedaje de sitios web estáticos en Azure Storage.
Nginx
El siguiente archivo nginx.conf se ha simplificado para mostrar cómo configurar Nginx para enviar el archivo
index.html siempre que no pueda encontrar un archivo correspondiente en el disco.

events { }
http {
server {
listen 80;

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
}
}
}

Para obtener más información sobre la configuración del servidor web de producción de Nginx, consulte Creating
NGINX Plus and NGINX Configuration Files (Creación de archivos de configuración de NGINX y NGINX Plus).
Nginx en Docker
Para hospedar Blazor en Docker mediante Nginx, configure el Dockerfile para usar la imagen de Nginx basada en
Alpine. Actualice el Dockerfile para copiar el archivo nginx.config en el contenedor.
Agregue una línea al Dockerfile, como se muestra en el ejemplo siguiente:

FROM nginx:alpine
COPY ./bin/Release/netstandard2.0/publish /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf

GitHub Pages
Para controlar las reescrituras de URL, agregue un archivo 404.html con un script que controle el
redireccionamiento de la solicitud a la página index.html. Para consultar una implementación de ejemplo que ha
proporcionado la comunidad, vea Single Page Apps for GitHub Pages (rafrex/spa-github-pages on GitHub)
(Aplicaciones de página única para GitHub Pages [rafrex/spa-github-pages en GitHub]). Se puede ver un ejemplo
del enfoque de la comunidad en blazor-demo/blazor-demo.github.io en GitHub (sitio activo).
Al usar un sitio de proyecto en lugar de un sitio de la organización, agregue o actualice la etiqueta <base> en
index.html. Defina el valor del atributo href con el nombre del repositorio de GitHub con una barra diagonal final
(por ejemplo, my-repository/ ).
Hospedaje e implementación de Blazor del lado
servidor
02/07/2019 • 2 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Valores de configuración de host


Las aplicaciones del lado servidor que usan el modelo de hospedaje del lado servidor pueden aceptar valores de
configuración del host genérico.

Implementación
Con el modelo de hospedaje del lado servidor, Blazor se ejecuta en el servidor desde una aplicación ASP.NET Core.
Las actualizaciones de la interfaz de usuario, el control de eventos y las llamadas de JavaScript se controlan
mediante una conexión de SignalR.
Se requiere un servidor web que pueda hospedar una aplicación ASP.NET Core. Visual Studio incluye la plantilla
de proyecto Blazor (servidor) ( blazorserverside cuando se usa el comando dotnet new ).

Escalabilidad horizontal de la conexión


Las aplicaciones de servidor de Blazor requieren una conexión de SignalR activa para cada usuario. Una
implementación de servidor de Blazor de producción requiere una solución para admitir tantas conexiones
simultáneas como requiera la aplicación. Azure SignalR Service controla el escalado de conexiones y se recomienda
como solución de escalado para las aplicaciones de servidor de Blazor. Para más información, consulte Publicar un
ASP.NET Core SignalR app en Azure App Service.

Configuración de SignalR
ASP.NET Core configura SignalR para los escenarios de servidor de Blazor más habituales. Para escenarios
personalizados y avanzados, consulte los artículos de SignalR de la sección Recursos adicionales.

Recursos adicionales
Introducción a ASP.NET Core SignalR
Documentación de Azure SignalR Service
Inicio rápido: Creación de un salón de chat con SignalR Service
Hospedaje e implementación de ASP.NET Core
Publicar una aplicación de ASP.NET Core en Azure con Visual Studio
Implementar una versión preliminar de ASP.NET Core en Azure App Service
Configuración del enlazador para ASP.NET Core
Blazor
04/07/2019 • 2 minutes to read • Edit Online

Por Luke Latham


Blazor realiza la vinculación de lenguaje intermedio (IL ) durante una compilación de versión para quitar el IL
innecesario de los ensamblados de salida de la aplicación.
Controle la vinculación del ensamblado con cualquiera de los enfoques siguientes:
Deshabilitación de la vinculación global con una propiedad de MSBuild.
Control de la vinculación por cada ensamblado con un archivo de configuración.

Deshabilitación de la vinculación con una propiedad de MSBuild


La vinculación se habilita de forma predeterminada en el modo de versión cuando se crea una aplicación, lo que
incluye la publicación. Para deshabilitar la vinculación para todos los ensamblados, establezca la propiedad
BlazorLinkOnBuild de MSBuild en false en el archivo de proyecto:

<PropertyGroup>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
</PropertyGroup>

Control de la vinculación con un archivo de configuración


Control de la vinculación por cada ensamblado al proporcionar un archivo de configuración XML y especificar el
archivo como un elemento MSBuild en el archivo de proyecto:

<ItemGroup>
<BlazorLinkerDescriptor Include="Linker.xml" />
</ItemGroup>

Linker.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!--
This file specifies which parts of the BCL or Blazor packages must not be
stripped by the IL Linker even if they aren't referenced by user code.
-->
<linker>
<assembly fullname="mscorlib">
<!--
Preserve the methods in WasmRuntime because its methods are called by
JavaScript client-side code to implement timers.
Fixes: https://github.com/aspnet/Blazor/issues/239
-->
<type fullname="System.Threading.WasmRuntime" />
</assembly>
<assembly fullname="System.Core">
<!--
System.Linq.Expressions* is required by Json.NET and any
expression.Compile caller. The assembly isn't stripped.
-->
<type fullname="System.Linq.Expressions*" />
</assembly>
<!--
In this example, the app's entry point assembly is listed. The assembly
isn't stripped by the IL Linker.
-->
<assembly fullname="MyCoolBlazorApp" />
</linker>

Para obtener más información, consulte IL Linker: Syntax of xml descriptor (Vinculador de IL: sintaxis del descriptor
xml).
Uso de la plantilla de proyecto de Angular con
ASP.NET Core
10/05/2019 • 14 minutes to read • Edit Online

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 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 Node.js esté instalado en el servidor (a menos que se ha habilitado la representación del
lado servidor (SSR )).
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.
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
}

Recursos adicionales
Introducción a la autenticación para aplicaciones de página única en ASP.NET Core
Uso de la plantilla de proyecto de React con
ASP.NET Core
10/05/2019 • 8 minutes to read • Edit Online

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 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. Agregar un .env del archivo a la ClientApp subdirectorio con la siguiente configuración:

BROWSER=none

Esto impedirá que el explorador web al abrir al iniciar el servidor CRA externamente.
2. En un símbolo del sistema, cambie al subdirectorio ClientApp e inicie el servidor de desarrollo de CRA:

cd ClientApp
npm start

3. 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.
IMPORTANT
"Representación del lado servidor" no es una característica compatible con esta plantilla. Nuestro objetivo con esta plantilla
es satisfacer la paridad con "create de react-app". Por lo tanto, escenarios y características no incluidas en un proyecto de
"creación de react-app" (por ejemplo, el SSR) no se admiten y se dejan como un ejercicio para el usuario.

Recursos adicionales
Introducción a la autenticación para aplicaciones de página única en ASP.NET Core
Uso de la plantilla de proyecto React-with-Redux con
ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

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.
Para obtener información sobre cómo configurar una aplicación secundaria React con Redux en IIS, consulte
ReactRedux plantilla 2.1: No se puede usar SPA en IIS (aspnet/Templating #555).
Usar servicios de JavaScript para crear aplicaciones
de página única en ASP.NET Core
07/06/2019 • 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 de SPA de lado cliente o bibliotecas, como Angular o reaccionar,
con los marcos del lado servidor como ASP.NET Core puede resultar difícil. Servicios de JavaScript 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.

Novedades en servicios de JavaScript


Servicios de JavaScript 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.
Servicios de JavaScript consta de dos paquetes de NuGet distintos:
Microsoft.AspNetCore.NodeServices (NodeServices)
Microsoft.AspNetCore.SpaServices (SpaServices)
Estos paquetes son útiles en los escenarios siguientes:
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 aplicaciones spa con
ASP.NET Core y no bloquea los desarrolladores en 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

Si la implementación en un sitio web de Azure, se requiere ninguna acción—Node.js está instalado y


disponible en los entornos de servidor.
.NET Core SDK 2.0 o posterior
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 de previos a la representación del lado servidor
Instalar el aspnet-procesamiento previo paquete npm:

npm i -S aspnet-prerendering

Configuración previos a la representación del lado servidor


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>

aplicación auxiliar de etiquetas ASP-prerender-module


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

aplicación auxiliar de etiquetas ASP-prerender-data


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

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 de software intermedio de desarrollo de Webpack


Instalar el aspnet webpack paquete npm:

npm i -D aspnet-webpack

Configuración de Webpack Dev Middleware


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 de sustitución del módulo activos
Instalar el middleware "hot" webpack paquete npm:
npm i -D webpack-hot-middleware

Configuración activa de sustitución del módulo


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, a menudo se desea el enrutamiento del lado cliente además del
enrutamiento del servidor. 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 lo—suele devolver un código de estado 404
de HTTP es el deseado.
Enrutamiento de requisitos previos de las aplicaciones auxiliares
Instale el paquete de npm de enrutamiento del lado cliente. Uso de Angular como ejemplo:

npm i -S @angular/router

Configuración de las aplicaciones auxiliares de enrutamiento


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

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 proyecto nuevo


Servicios de JavaScript proporcionan plantillas de aplicación previamente configurada. SpaServices se usa en
estas plantillas junto con diferentes marcos y bibliotecas como Angular y React con 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. Para obtener más información, consulte establecer el entorno.
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.

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

Prueba de 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. Copie los recursos generados por Webpack en la carpeta de publicación.
El destino de MSBuild se invoca cuando se ejecuta:

dotnet publish -c Release

Recursos adicionales
Docs angulares
Adquisición de bibliotecas del lado cliente en
ASP.NET Core con LibMan
17/05/2019 • 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 más información sobre las ventajas de LibMan, consulte Modern front-end web development in Visual
Studio 2017: LibMan segment (Desarrollo web de 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
10/05/2019 • 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
14/05/2019 • 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 de 2019 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) lib/jquery/

Páginas carpeta del proyecto Pages/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
Uso de Grunt en ASP.NET Core
19/06/2019 • 17 minutes to read • Edit Online

Grunt es un ejecutor de tareas de JavaScript que automatiza la minificación de secuencia de comandos,


compilación de TypeScript, las herramientas "lint" de calidad de código, preprocesadores CSS y casi cualquier
tarea repetitiva que necesita hacer para admitir el desarrollo cliente. Grunt es totalmente compatible en Visual
Studio.
Este ejemplo usa un proyecto vacío de ASP.NET Core como punto de partida, para mostrar cómo automatizar el
proceso de compilación de cliente desde el principio.
El ejemplo finalizado 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 se implementa en la raíz de la
aplicación web. Usamos los siguientes paquetes:
grunt: El paquete de ejecutor de tareas de Grunt.
grunt-contrib-clean: Un complemento que quita los archivos o directorios.
grunt-contrib-jshint: Un complemento que revisa la calidad del código JavaScript.
grunt-contrib-concat: Un complemento que combina los archivos en un único archivo.
grunt-contrib-uglify: Un complemento que minifica el objeto de JavaScript para reducir el tamaño.
grunt-contrib-watch: Un complemento que supervisa la actividad de archivos.

Preparación de la aplicación
Para empezar, configure una nueva aplicación web vacía y agregar los 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 la prima para procesar el uso de Grunt.
1. En Visual Studio, cree un nuevo ASP.NET Web Application .
2. En el nuevo proyecto ASP.NET cuadro de diálogo, seleccione ASP.NET Core 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. Agregue 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
TypeScript se protegió ningún archivo. 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 asigne el nombre Tastes.ts (tenga en cuenta el *extensión
TS ). 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 Network Performance Monitor para descargar grunt y tareas de 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 de elemento, deje el nombre
predeterminado, package.jsony haga clic en el agregar botón.
2. En el package.json de archivos, en el devDependencies objetos entre llaves, escriba "grunt". Seleccione
grunt en Intellisense lista y presione la tecla ENTRAR. Visual Studio entrecomillar el nombre del paquete
de grunt y agregue dos puntos. A la derecha de los dos puntos, seleccione la última versión estable del
paquete en la parte superior de la lista de Intellisense (presione Ctrl-Space si Intellisense no aparece).

NOTE
NPM usa 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 (0.4.5 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. 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 para cargar grunt-contrib -* empaqueta para su limpia, jshint, concat, a que
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 devDependencies elemento descargará, junto con los archivos que requiere cada paquete.
Puede encontrar los archivos del paquete en el node_modules directorio habilitando la mostrar todos los
archivos botón 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ú.
Configuración de 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 JavaScript
plantilla de elemento, cambie el nombre a Gruntfile.jsy haga clic en el agregar botón.
2. Agregue el código siguiente al Gruntfile.js. El initConfig función establece opciones para cada paquete, y el
resto del módulo de carga y registrar las tareas.

module.exports = function (grunt) {


grunt.initConfig({
});
};

3. Dentro de la initConfig funcione, agregue las opciones para la clean tareas tal como se muestra en el
ejemplo Gruntfile.js a continuación. El clean tarea acepta una matriz de cadenas de directorio. Esta tarea
elimina los archivos de wwwroot/lib y quita toda la /temp directory.

module.exports = function (grunt) {


grunt.initConfig({
clean: ["wwwroot/lib/*", "temp/"],
});
};

4. A continuación el initConfig de función, agregue una llamada a grunt.loadNpmTasks . Esto hará que la tarea
se puede ejecutar desde Visual Studio.

grunt.loadNpmTasks("grunt-contrib-clean");

5. Guardar Gruntfile.js. El archivo debe ser similar a la captura de pantalla siguiente.

6. Haga clic en Gruntfile.js y seleccione Task Runner Explorer en el menú contextual. El Task Runner
Explorer se abrirá la ventana.

7. Compruebe que clean se muestra bajo tareas en el Task Runner Explorer.

8. 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 crearlas manualmente en el Explorador
de soluciones y, a continuación, ejecutar la tarea clean como prueba.

9. En el initConfig de función, 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 de Grunt
para permitir que varios entornos de compilación. Puede ver los destinos integrados mediante IntelliSense o asignar
su propio.
10. Agregue el jshint de tareas con el código siguiente.
El jshint code-quality utilidad se ejecuta en cada archivo de JavaScript que se encuentra en la temp
directory.

jshint: {
files: ['temp/*.js'],
options: {
'-W069': false,
}
},

NOTE
La opción "-W069" es un error generado por jshint cuando JavaScript usa la sintaxis para asignar una propiedad en
lugar de la notación de puntos, es decir, los corchetes Tastes["Sweet"] en lugar de Tastes.Sweet . La opción
desactiva la advertencia para permitir que el resto del proceso para continuar.

11. Agregue el uglify de tareas con el código siguiente.


La tarea minifica objeto el combined.js archivo se encuentra en el directorio temporal y crea el archivo de
resultados en wwwroot/lib siguiendo la convención de nomenclatura estándar <nombre de archivo>.
min.js.

uglify: {
all: {
src: ['temp/combined.js'],
dest: 'wwwroot/lib/combined.min.js'
}
},

12. En la llamada a grunt.loadNpmTasks que carga grunt-contrib-clean , incluir la misma llamada para jshint,
concat y a que uglify con el código siguiente.

grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');

13. Guardar Gruntfile.js. El archivo debe tener un aspecto similar al ejemplo siguiente.
14. Tenga en cuenta que el Task Runner Explorer lista de tareas incluye clean , concat , jshint y uglify
tareas. Cada tarea se ejecutan en orden y observe los resultados en el Explorador de soluciones. Cada
tarea debe ejecutarse sin errores.

La tarea de concat crea un nuevo combined.js de archivo y lo coloca en el directorio temporal. El jshint
tarea simplemente se ejecuta y no genera resultados. El uglify crea una nueva tarea combined.min.js de
archivo y lo coloca en wwwroot/lib. Una vez completada, 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-contrib-clean para obtener un vínculo a documentación que explica todos sus parámetros.

Ahora todos juntos


Utilice el Grunt registerTask() método para ejecutar una serie de tareas en una secuencia determinada. Por
ejemplo, para ejecutar el ejemplo de pasos anteriores en el orden limpio -> concat -> jshint -> a que uglify,
agregue el siguiente código al módulo. El código debe agregarse en el mismo nivel que las llamadas de
loadNpmTasks() fuera initConfig.

grunt.registerTask("all", ['clean', 'concat', 'jshint', 'uglify']);

La nueva tarea aparece en el Explorador de ejecutores de tareas en tareas de Alias. Puede haga clic en y ejecutarla
como lo haría en otras tareas. El all se ejecutará la tarea clean , concat , jshint y uglify , en orden.

Observación de cambios
Un watch tarea cuida en archivos y directorios. La inspección desencadena tareas automáticamente si detecta los
cambios. Agregue el código siguiente para initConfig para ver los cambios a *archivos .js en el directorio de
TypeScript. Si se cambia un archivo JavaScript, watch se ejecutará la all tarea.

watch: {
files: ["TypeScript/*.js"],
tasks: ["all"]
}

Agregue una llamada a loadNpmTasks() para mostrar la watch tarea en Task Runner Explorer.
grunt.loadNpmTasks('grunt-contrib-watch');

Haga clic en la tarea de inspección en Task Runner Explorer y seleccione Ejecutar en el menú contextual. La
ventana de comandos que se muestra la ejecución de tareas de inspección mostrará un bucle "esperando..."
Mensaje. Abra uno de los archivos de TypeScript, agregue un espacio y, a continuación, guarde el archivo. Esto
desencadenará 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 cada vez que se trabaja en Visual Studio, puede enlazar tareas a
antes de compilar, después de compilar, Clean, y Proyecto abierto eventos.
Vamos a enlazar watch para que se ejecute cada vez que abra Visual Studio. En el Explorador de ejecutores de
tareas, haga clic en la tarea de inspección y seleccione enlaces > Abrir proyecto en el menú contextual.

Descargar y recargar el proyecto. Cuando se carga el proyecto de nuevo, iniciará la tarea de inspección se ejecuten
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. Visual Studio Task Runner Explorer detecta los cambios en los 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.
Administrar los paquetes del lado cliente con Bower
en ASP.NET Core
10/05/2019 • 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: Un 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.
Agrupar y minificar recursos estáticos en ASP.NET
Core
18/06/2019 • 18 minutes to read • Edit Online

Por Scott Addie y David Borovice


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(t,a){var r=$(t,a);r.attr("alt",r.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 Grunt ejecutor 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


En ASP.NET Core 2.0 o versiones anteriores, las plantillas de proyecto MVC y páginas de Razor proporcionan una
bundleconfig.json archivo de configuración que define las opciones para cada lote:
En ASP.NET Core 2.1 o posterior, agregue un nuevo archivo JSON, denominado bundleconfig.json, a la raíz del
proyecto MVC o las páginas de Razor. Incluya el siguiente JSON en ese archivo como un punto de partida:
[
{
"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
}
]

El bundleconfig.json archivo define las opciones para cada paquete. En el ejemplo anterior, 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) los archivos.
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. required
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.
optional, default - false
sourceMap : Marca que indica si se debe generar un mapa de origen para el archivo agrupado. optional, default
- 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:
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>

<environment names="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:

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

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

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:

WARNING
El gulp-uglify módulo no es compatible con ECMAScript (ES) 2015 / ES6 y versiones posteriores. Instalar gulp viste en
lugar de gulp-uglify usar ES2015 / ES6 o una versión posterior.

"devDependencies": {
"del": "^3.0.0",
"gulp": "^4.0.0",
"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');

const regex = {
css: /\.css$/,
html: /\.(html|htm)$/,
js: /\.js$/
};

gulp.task('min:js', async function () {


merge(getBundles(regex.js).map(bundle => {
return gulp.src(bundle.inputFiles, { base: '.' })
.pipe(concat(bundle.outputFileName))
.pipe(uglify())
.pipe(gulp.dest('.'));
}))
});

gulp.task('min:css', async function () {


merge(getBundles(regex.css).map(bundle => {
return gulp.src(bundle.inputFiles, { base: '.' })
.pipe(concat(bundle.outputFileName))
.pipe(cssmin())
.pipe(gulp.dest('.'));
}))
});

gulp.task('min:html', async function () {


merge(getBundles(regex.html).map(bundle => {
return gulp.src(bundle.inputFiles, { base: '.' })
.pipe(concat(bundle.outputFileName))
.pipe(htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))
.pipe(gulp.dest('.'));
}))
});

gulp.task('min', gulp.series(['min:js', 'min:css', 'min:html']));

gulp.task('clean', () => {
return del(bundleconfig.map(bundle => bundle.outputFileName));
});

gulp.task('watch', () => {
getBundles(regex.js).forEach(
bundle => gulp.watch(bundle.inputFiles, gulp.series(["min:js"])));

getBundles(regex.css).forEach(
bundle => gulp.watch(bundle.inputFiles, gulp.series(["min:css"])));

getBundles(regex.html).forEach(
bundle => gulp.watch(bundle.inputFiles, gulp.series(['min:html'])));
});
const getBundles = (regexPattern) => {
return bundleconfig.filter(bundle => {
return regexPattern.test(bundle.outputFileName);
});
};

gulp.task('default', gulp.series("min"));

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 ==========

Recursos adicionales
Uso de Grunt
Uso de varios entornos
Asistentes de etiquetas
Vínculo de explorador en ASP.NET Core
10/05/2019 • 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: Vínculo de explorador está deshabilitado 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.
Estado de sesión y aplicación en ASP.NET Core
06/06/2019 • 30 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. Los
datos críticos de aplicaciones deben almacenarse en la base de datos de usuario y almacenarse en caché en la
sesión solo para optimizar el rendimiento.

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.
Las plantillas de Razor Pages y ASP.NET Core MVC guardan conformidad con el Reglamento general de
protección de datos (RGPD ). Las cookies de estado de sesión no se marcan como esenciales de forma
predeterminada, por lo que el estado de sesión no será funcional a menos que el visitante del sitio permita el
seguimiento. Para obtener más información, vea Reglamento de protección de datos generales (RGPD ) se
admiten en ASP.NET Core.

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
Almacenamiento en caché en ASP.NET Core distribuido.
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 más información, vea Protección de datos de ASP.NET
Core y 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:
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 Almacenamiento en caché en
ASP.NET Core distribuido.
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.AddDistributedMemoryCache();

services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
// Make the session cookie essential
options.Cookie.IsEssential = true;
});

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseHttpContextItemsMiddleware();
app.UseMvc();
}
}

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

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. Este valor 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. Este valor 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 ).
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_2);

services.AddSession(options =>
{
options.Cookie.Name = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.IsEssential = true;
});
}

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

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

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

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
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.
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 de ASP.NET Core y 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_2)
.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 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>();
}
}

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!";

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 Almacenamiento en caché de respuesta en ASP.NET Core.

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

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 Almacenamiento en caché en ASP.NET Core distribuido y Almacenar en
caché en memoria en ASP.NET Core.
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
Diseño en ASP.NET Core
29/04/2019 • 11 minutes to read • Edit Online

Por Steve Smith y Dave Brock


Las páginas y las vistas a menudo comparten elementos visuales y elementos mediante programación.
En este artículo se explica cómo:
Usar diseños comunes.
Compartir directivas.
Ejecutar código común antes de representar páginas o vistas.
En este documento se analizan los diseños para los dos enfoques distintos para ASP.NET Core MVC:
Razor Pages y controladores con vistas. Para este tema, las diferencias son mínimas:
Razor Pages está en la carpeta Páginas.
Los controladores con vistas usan una carpeta Vistas para las vistas.

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.
Por convención, el diseño predeterminado para una aplicación ASP.NET Core se denomina
_Layout.cshtml. El archivo de diseño para los nuevos proyectos de ASP.NET Core creados con las
plantillas:
Razor Pages: Pages/Shared/_Layout.cshtml

Controlador con vistas: Views/Shared/_Layout.cshtml

Este diseño define una plantilla de nivel superior para las vistas en la aplicación. Las aplicaciones no
necesitan un diseño. Las aplicaciones pueden definir más de un diseño con distintas vistas que
especifiquen diseños diferentes.
Este código muestra el archivo de diseño para un proyecto creado mediante plantilla con un
controlador y vistas:

<!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 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>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">WebApplication1</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>
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - WebApplication1</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-
tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
</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>

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 (por ejemplo,
/Pages/Shared/_Layout.cshtml o /Views/Shared/_Layout.cshtml) o un nombre parcial (ejemplo:
_Layout ). Cuando se proporciona un nombre parcial, el motor de vista de Razor busca el archivo de
diseño mediante su proceso de detección estándar. Primero se busca la carpeta donde existe el método
de controlador (o controlador), seguida de la carpeta Shared. Este proceso de detección es idéntico al
que se usa 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:

@section Scripts {
@RenderSection("Scripts", required: false)
}

Si no se encuentra una sección obligatoria, se produce 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
página o una vista define una sección, se debe representar (o se producirá un error).
Ejemplo de definición de @section en una vista de Razor Pages:

@section Scripts {
<script type="text/javascript" src="/scripts/main.js"></script>
}

En el código anterior, scripts/main.js se agrega a la sección scripts en una página o vista. Es posible
que otras páginas o vistas de la misma aplicación no necesiten este script y no definan una sección de
script.
El marcado siguiente usa el asistente de etiquetas parcial para representar
_ValidationScriptsPartial.cshtml:

@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

El marcado anterior se ha generado mediante la identidad de scaffolding.


Las secciones definidas en una vista o 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 y las páginas pueden usar directivas de Razor para la importación de espacios de nombres y
usar la 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 Pages (o Views). Un archivo _ViewImports.cshtml puede colocarse dentro de cualquier carpeta,
en cuyo caso solo se aplicará a páginas o 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 llevó a la ubicación
de la propia página o vista. La configuración _ViewImports especificada en el nivel de raíz se puede
reemplazar en el nivel de carpeta.
Por ejemplo, supongamos que:
El archivo _ViewImports.cshtml del nivel de raíz incluye @model MyModel1 y
@addTagHelper *, MyTagHelper1 .
Un archivo _ViewImports.cshtml de subcarpeta incluye @model MyModel2 y
@addTagHelper *, MyTagHelper2 .

Las páginas y las vistas de la subcarpeta tendrán acceso a los asistentes de etiquetas y al modelo
MyModel2 .

Si hay varios _ViewImports.cshtml en la jerarquía de archivos, el comportamiento combinado de las


directivas es:
@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
@inject : para cada propiedad, la más cercana a la vista invalida cualquier otra con el mismo
nombre de propiedad

Ejecutar código antes de cada vista


El código que debe ejecutarse antes de cada vista o página se debería colocar en el archivo
_ViewStart.cshtml. Por convención, el archivo _ViewStart.cshtml se encuentra en la carpeta Pages (o
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 o de página, se
ejecutará después del que esté definido en la raíz de la carpeta Pages (o Views) (si existe).
Un archivo _ViewStart.cshtml de ejemplo:

@{
Layout = "_Layout";
}

El archivo anterior especifica que todas las vistas usarán el diseño _Layout.cshtml.
_ViewStart.cshtml y _ViewImports.cshtml normalmente no se colocan en la carpeta /Pages/Shared (o
/Views/Shared). Las versiones de nivel de aplicación de estos archivos deben colocarse directamente en
la carpeta /Pages (o /Views).
Referencia de sintaxis de Razor para ASP.NET
Core
18/06/2019 • 28 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>

En los bloques de código, declare las funciones locales con marcado para utilizarlas como métodos en la
creación de plantillas:

@{
void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}

RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}

El código representa el siguiente HTML:

<p>Name: <strong>Mahatma Gandhi</strong></p>


<p>Name: <strong>Martin Luther King, Jr.</strong></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 asistentes de HTML que incluyen contenido adicional. En el siguiente
código, los asistentes de 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 los asistentes 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 Inspecció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

Los métodos @functions se pueden usar para la creación de plantillas si están marcados:

@{
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}

@functions {
private void RenderName(string name)
{
<p>Name: <strong>@name</strong></p>
}
}

El código representa el siguiente HTML:

<p>Name: <strong>Mahatma Gandhi</strong></p>


<p>Name: <strong>Martin Luther King, Jr.</strong></p>

@attribute
La directiva @attribute agrega el atributo especificado a la clase de la página o vista generada. En el
ejemplo siguiente se agrega el atributo [Authorize] :

@attribute [Authorize]

WARNING
En la versión preliminar 6 de ASP.NET Core 3.0, hay un problema conocido en el que las directivas @attribute no
funcionan en los archivos _Imports.razor y _ViewImports.cshtml. Esto se solucionará en la versión preliminar 7.

@namespace
La directiva @namespace establece el espacio de nombres de la clase de la página o vista generada:

@namespace Your.Namespace.Here
Si una página o vista importa API con una directiva @namespace , el espacio de nombres del archivo original
se establece en relación con ese espacio de nombres.
Si MyApp/Pages/_ViewImports.cshtml contiene @namespace Hello.World , el espacio de nombres de las
páginas o vistas que importan el espacio de nombres Hello.World se establece como se muestra en la
tabla siguiente.

PÁGINA (O VISTA) ESPACIO DE NOMBRES

MyApp/Pages/Index.cshtml Hello.World

MyApp/Pages/MorePages/Bar.cshtml Hello.World.MorePages

Si varios archivos de importación tienen la directiva @namespace , se usa el archivo más cercano a la página
o vista en la cadena del directorio.
@section
La directiva @section se usa junto con el diseño para permitir que las páginas o vistas representen el
contenido en otras partes de la página HTML. Para más información, vea Sections (Secciones).

Delegados con plantillas de Razor


Las plantillas de Razor permiten definir un fragmento de la interfaz de usuario con el formato siguiente:

@<tag>...</tag>

En el ejemplo siguiente se muestra cómo especificar un delegado de Razor con plantilla como elemento
Func<T,TResult>. El tipo dinámico se especifica para el parámetro del método encapsulado por el
delegado. Se especifica un tipo de objeto como el valor devuelto del delegado. La plantilla se usa con un
elemento List<T> de Pet que tiene una propiedad Name .

public class Pet


{
public string Name { get; set; }
}

@{
Func<dynamic, object> petTemplate = @<p>You have a pet named <strong>@item.Name</strong>.</p>;

var pets = new List<Pet>


{
new Pet { Name = "Rin Tin Tin" },
new Pet { Name = "Mr. Bigglesworth" },
new Pet { Name = "K-9" }
};
}

La plantilla se representa con el elemento pets proporcionado por una instrucción foreach :

@foreach (var pet in pets)


{
@petTemplate(pet)
}
Salida representada:

<p>You have a pet named <strong>Rin Tin Tin</strong>.</p>


<p>You have a pet named <strong>Mr. Bigglesworth</strong>.</p>
<p>You have a pet named <strong>K-9</strong>.</p>

También se puede proporcionar una plantilla de Razor insertada como un argumento para un método. En
el ejemplo siguiente, el método Repeat recibe una plantilla de Razor. El método usa la plantilla para
generar contenido HTML con repeticiones de elementos proporcionados a partir de una lista:

@using Microsoft.AspNetCore.Html

@functions {
public static IHtmlContent Repeat(IEnumerable<dynamic> items, int times,
Func<dynamic, IHtmlContent> template)
{
var html = new HtmlContentBuilder();

foreach (var item in items)


{
for (var i = 0; i < times; i++)
{
html.AppendHtml(template(item));
}
}

return html;
}
}

Con la lista de mascotas del ejemplo anterior, se llama al método Repeat con:
List<T> de Pet .
Número de veces que se repite cada mascota.
Plantilla insertada que se va a usar para los elementos de una lista sin ordenar.

<ul>
@Repeat(pets, 3, @<li>@item.Name</li>)
</ul>

Salida representada:

<ul>
<li>Rin Tin Tin</li>
<li>Rin Tin Tin</li>
<li>Rin Tin Tin</li>
<li>Mr. Bigglesworth</li>
<li>Mr. Bigglesworth</li>
<li>Mr. Bigglesworth</li>
<li>K-9</li>
<li>K-9</li>
<li>K-9</li>
</ul>

Asistentes de etiquetas
Hay tres directivas que pertenecen a los asistentes de etiquetas.
DIRECTIVA FUNCIÓN

@addTagHelper Pone los asistentes de etiquetas a disposición de una


vista.

@removeTagHelper Quita los asistentes de etiquetas agregadas anteriormente


desde una vista.

@tagHelperPrefix Especifica una cadena de prefijo de etiqueta para permitir


la compatibilidad con el asistente 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

Inspección de la clase C# de Razor generada por una vista


Con el SDK de .NET Core 2.1 o posterior, el SDK de Razor controla la compilación de los archivos de Razor.
Al compilar un proyecto, el SDK de Razor genera un directorio
obj/<configuración_de_compilación>/<moniker_de_la_plataforma_de_destino>/Razor en la raíz del
proyecto. La estructura de directorios dentro del directorio Razor refleja la del proyecto.
Tenga en cuenta la estructura de directorios siguiente en un proyecto de Razor Pages de ASP.NET Core 2.1
destinado a .NET Core 2.1:
Areas/
Admin/
Pages/
Index.cshtml
Index.cshtml.cs
Pages/
Shared/
_Layout.cshtml
_ViewImports.cshtml
_ViewStart.cshtml
Index.cshtml
Index.cshtml.cs
Al compilar el proyecto en la configuración Depurar se crea el directorio obj siguiente:
obj/
Debug/
netcoreapp2.1/
Razor/
Areas/
Admin/
Pages/
Index.g.cshtml.cs
Pages/
Shared/
_Layout.g.cshtml.cs
_ViewImports.g.cshtml.cs
_ViewStart.g.cshtml.cs
Index.g.cshtml.cs
Para ver la clase generada para Pages/Index.cshtml, abra
obj/Debug/netcoreapp2.1/Razor/Pages/Index.g.cshtml.cs.
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;
}
}

En Startup.ConfigureServices , 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:
Nombres de acciones, controladores y áreas.
Páginas de Razor.
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.
Crear la interfaz de usuario reutilizable con el
proyecto de biblioteca de clases de Razor en
ASP.NET Core
28/06/2019 • 13 minutes to read • Edit Online

Por Rick Anderson


Las vistas de Razor, páginas, controladores, modelos de página, componentes Razor, ver componentes, y se
pueden generar modelos de datos 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.
Un RCL 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 RCL
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: Cree un proyecto RCL y use 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 un RCL
En esta sección, se crea un 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 que la biblioteca de clases de interfaz de usuario de Razor está en uso:
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 RCL, 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 RCL.
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>

Crear un RCL con activos estáticos


Un RCL puede requerir recursos estáticos de complementaria que pueden hacer referencia a la aplicación de
consumo de la RCL. ASP.NET Core permite crear RCLs que incluyen recursos estáticos que están disponibles
para una aplicación que lo consume.
Para incluir recursos complementarios como parte de un RCL, cree un wwwroot carpeta en la biblioteca de
clases e incluir los archivos necesarios en esa carpeta.
Cuando se empaqueta un RCL, companion en todos los recursos en el wwwroot carpeta se incluyen
automáticamente en el paquete y se ponen a disposición para hacer referencia al paquete de aplicaciones.
Consumir contenido de un RCL que se hace referencia
Los archivos incluidos en el wwwroot carpeta de la RCL están expuestas a la aplicación que lo consume en el
prefijo _content/{LIBRARY NAME}/ . {LIBRARY NAME} es el nombre del proyecto de biblioteca que se convierte en
minúsculas con períodos ( . ) eliminado. Por ejemplo, una biblioteca denominada Razor.Class.Lib da como
resultado una ruta de acceso a contenido estático en _content/razorclasslib/ .
La aplicación que hace referencia a recursos estáticos proporcionados por la biblioteca con <script> , <style> ,
<img> y otras etiquetas HTML. Debe tener la aplicación consumidora compatibilidad con archivos estáticos
habilitado.
Flujo de desarrollo de varios proyectos
Cuando se ejecuta la aplicación de consumo:
Los activos de la estancia RCL en sus carpetas originales. Los recursos no se mueven a la aplicación que lo
consume.
Cualquier cambio en el RCL wwwroot carpeta se refleja en la aplicación consumidora después de que se
vuelve a generar el RCL y sin volver a generar la aplicación que lo consume.
Cuando se compila el RCL, se genera un manifiesto que describe las ubicaciones de recurso web estático. La
aplicación que lee el manifiesto en tiempo de ejecución para consumir los recursos de los paquetes y proyectos
que se hace referencia. Cuando se agrega un nuevo recurso a un RCL, se debe regenerar el RCL para actualizar
su manifiesto antes de una aplicación que pueda acceder el nuevo recurso.
Publicar
Cuando se publica la aplicación, los recursos complementarios de paquetes y proyectos de todos los que se
hace referencia se copian en el wwwroot carpeta de la aplicación publicada en _content/{LIBRARY NAME}/ .
Asistentes de etiquetas integradas de ASP.NET Core
29/04/2019 • 2 minutes to read • Edit Online

Por Peter Kellner


Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.

NOTE
Hay asistentes de etiquetas integradas que no se describen en la documentación. Estos asistentes de etiquetas se usan
internamente por el motor de vista 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 acción 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
Asistentes de etiquetas en ASP.NET Core
Componentes del asistente de etiquetas en ASP.NET Core
Asistentes de etiquetas en ASP.NET
Core
10/05/2019 • 25 minutes to read • Edit Online

Por Rick Anderson

Qué son los asistentes de etiquetas


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. 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 muchos
asistentes de etiquetas integradas para tareas comunes (como la creación de
formularios, vínculos, carga de activos, etc.) y existen muchos más a disposición en
repositorios públicos de GitHub y como paquetes NuGet. Los asistentes 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
asistentes de HTML, los asistentes de etiquetas reducen las transiciones explícitas entre
HTML y C# en las vistas de Razor. En muchos casos, los asistentes de HTML
proporcionan un método alternativo para un asistente de etiquetas específico, pero es
importante tener en cuenta que los asistentes de etiquetas no reemplazan a los
asistentes de HTML y que no hay un asistente de etiquetas para cada asistente de
HTML. En Comparación entre los asistentes de etiquetas y los asistentes de HTML se
explican las diferencias con más detalle.

¿Qué proporcionan los asistentes de etiquetas?


Una experiencia de desarrollo compatible con HTML La mayor parte del
marcado de Razor con asistentes 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 los asistentes de HTML, el método anterior para la
creación en el lado servidor de marcado en vistas de Razor. En Comparación entre los
asistentes de etiquetas y los asistentes de HTML se explican las diferencias con más
detalle. En Compatibilidad de IntelliSense con asistentes 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 asistentes 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 los asistentes 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 asistentes de etiquetas para más información.

Administración del ámbito de los asistentes de


etiquetas
El ámbito de los asistentes de etiquetas se controla mediante una combinación de
@addTagHelper , @removeTagHelper y el carácter de exclusión "!".

@addTagHelper hace que los asistentes 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 los asistentes 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 los asistentes de etiquetas estén disponibles. El código
anterior usa la sintaxis de comodines ("*") para especificar que todas los asistentes 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 los asistentes de etiquetas que
se van a cargar (usamos "*" para todas los asistentes de etiquetas), y el segundo
parámetro ("Microsoft.AspNetCore.Mvc.TagHelpers") especifica el ensamblado que
contiene los asistentes de etiquetas. Microsoft.AspNetCore.Mvc.TagHelpers es el
ensamblado para los asistentes de etiquetas integradas de ASP.NET Core.
Para exponer todas los asistentes 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 un asistente EmailTagHelper con el espacio de nombres


predeterminado ( AuthoringTagHelpers.TagHelpers.EmailTagHelper ), puede proporcionar
el nombre completo (FQN ) del asistente de etiquetas:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers

Para agregar un asistente 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 el asistente 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 el asistente
de etiquetas solo a esas vistas.
@removeTagHelper quita los asistentes de etiquetas
@removeTagHelper tiene los mismos parámetros que @addTagHelper , y quita un
asistente de etiquetas que se ha agregado anteriormente. Por ejemplo, si se aplica
@removeTagHelper a una vista específica, se quita de la vista el asistente de etiquetas
especificada. Si se usa @removeTagHelper en un archivo
Views/Folder/_ViewImports.cshtml, se quita el asistente de etiquetas especificada de
todas las vistas de Folder.
Controlar el ámbito del asistente 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
esos asistentes de etiquetas únicamente a las vistas de la carpeta Home.
Excluir elementos individuales
Puede deshabilitar un asistente de etiquetas en el nivel de elemento con el carácter de
exclusión ("!") del asistente de etiquetas. Por ejemplo, la validación de Email se
deshabilita en <span> con el carácter de exclusión del asistente de etiquetas:

<!span asp-validation-for="Email" class="text-danger"></!span>

Debe aplicar el carácter de exclusión del asistente 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 del asistente de
etiquetas ya no se muestran en una fuente distinta.
Usar @tagHelperPrefix para hacer explícito el uso del asistente de etiquetas
La directiva @tagHelperPrefix permite especificar una cadena de prefijo de etiqueta
para habilitar la compatibilidad con el asistente 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 del asistente de etiquetas se establece en


th: , por lo que solo los elementos con el prefijo th: admiten asistentes de etiquetas
(los elementos habilitados para asistentes de etiquetas tienen una fuente distinta). Los
elementos <label> y <input> tienen el prefijo de los asistentes 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 .

Asistentes de etiquetas de autocierre


Muchos asistentes de etiquetas no se pueden usar como etiquetas de autocierre.
Algunos asistentes de etiquetas están diseñados para ser etiquetas de cierre. Si se usa
un asistente de etiquetas que no se ha diseñado para ser de autocierre se suprime la
salida representada. Si se usa el autocierre para un asistente de etiquetas, se genera
una etiqueta de autocierre en la salida representada. Vea esta nota en Creación de
asistentes de etiquetas para más información.

Compatibilidad de IntelliSense con asistentes 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 los asistentes 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 los asistentes 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 al asistente 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 del asistente 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 los asistentes de etiquetas y los


asistentes de HTML
Los asistentes de etiquetas se asocian a elementos HTML en las vistas de Razor,
mientras que los asistentes de 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 clave 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:

<label class="caption" asp-for="FirstName"></label>

Con la versión del asistente 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.


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 de MVC de ASP.NET
4.5.x incluida con Visual Studio.
En el editor de Visual Studio se muestra el código de C# con un fondo gris. Por
ejemplo, el asistente de 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 asistentes 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 asistentes de 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 un asistente 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 del


asistente 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 asistentes
de HTML. En Compatibilidad de IntelliSense con asistentes de etiquetas se explica en
detalle cómo trabajar con asistentes de etiquetas en el editor de Visual Studio.

Comparación entre los asistentes de etiquetas y los


controles de servidor web
Los asistentes 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. Los asistentes de etiquetas no tienen ningún DOM.
Los controles de servidor web incluyen la detección automática del explorador.
Los asistentes de etiquetas no tienen conocimiento del explorador.
Varios asistentes de etiquetas pueden actuar en el mismo elemento (vea Evitar
conflictos de asistentes de etiquetas), mientras que normalmente no se pueden
crear controles de servidor web.
Los asistentes 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 los asistentes 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 los asistentes 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 asistentes de


etiquetas
Puede personalizar la fuente y el color en Herramientas > Opciones > Entorno >
Fuentes y colores:

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
FormActionTagHelper
Asistente de etiquetas de formulario
Asistente de etiquetas de acción 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
Creación de asistentes de etiquetas
Trabajar con formularios
TagHelperSamples en GitHub contiene ejemplos de asistentes de etiquetas para
trabajar con Bootstrap.
Crear asistentes de etiquetas en ASP.NET Core
21/05/2019 • 29 minutes to read • Edit Online

Por Rick Anderson


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

Introducción a los asistentes de etiquetas


En este tutorial se proporciona una introducción a la programación de asistentes de etiquetas. En Introducción a
los asistentes de etiquetas se describen las ventajas que proporcionan los asistentes de etiquetas.
Un asistente de etiquetas es una clase que implementa la interfaz ITagHelper . A pesar de ello, cuando se crea
un asistente 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 los asistentes de etiquetas denominada TagHelpers. La carpeta
TagHelpers no es necesaria, pero es una convención razonable. Ahora vamos a empezar a escribir
algunos asistentes de etiquetas simples.

Asistente de etiquetas mínima


En esta sección, escribirá un asistente de etiquetas que actualice una etiqueta de correo electrónico. Por ejemplo:

<email>Support</email>

El servidor usará nuestro asistente 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
}
}
}

Los asistentes 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 los asistentes 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 asistentes de etiquetas.
El método Process invalidado controla lo que hace el asistente 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:

@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers

El código anterior usa la sintaxis de comodines para especificar que todos los asistentes de etiquetas del
ensamblado estarán disponibles. La primera cadena después de @addTagHelper especifica el asistente de
etiquetas que se va a cargar (use "*" para todos los asistentes de etiquetas), mientras que la segunda
cadena "AuthoringTagHelpers" especifica el ensamblado en el que se encuentra el asistente de etiquetas.
Además, tenga en cuenta que la segunda línea incorpora los asistentes de etiquetas de ASP.NET Core
MVC mediante la sintaxis de comodines (esos asistentes se tratan en el tema Introducción a los asistentes
de etiquetas). Es la directiva @addTagHelper la que hace que el asistente de etiquetas esté disponible para
la vista de Razor. Como alternativa, puede proporcionar el nombre completo (FQN ) de un asistente de
etiquetas como se muestra a continuación:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers

Para agregar un asistente de etiquetas a una vista con un FQN, agregue primero el FQN (
AuthoringTagHelpers.TagHelpers.EmailTagHelper ) y, después, el nombre del ensamblado ( AuthoringTagHelpers,
no necesariamente namespace ). La mayoría de los desarrolladores prefiere usar la sintaxis de comodines. En
Introducción a los asistentes de etiquetas se describe en detalle la adición y eliminación de asistentes de
etiquetas, la jerarquía y la sintaxis de comodines.
1. 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>

2. 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="..." />.


// PascalCase gets translated into 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);
}
}

Los nombres de clase y propiedad con grafía Pascal para los asistentes de etiquetas se convierten a su
grafía kebab. Por tanto, para usar el atributo MailTo , usará su equivalente <email mail-to="value"/> .
La última línea establece el contenido completado para nuestro asistente 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 del asistente 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:

@{
ViewData["Title"] = "Contact Copy";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<address>
One Microsoft Way Copy Version <br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>

<address>
<strong>Support:</strong><email mail-to="Support"></email><br />
<strong>Marketing:</strong><email mail-to="Marketing"></email>
</address>

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 un asistente 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 un asistente de etiquetas de autocierre. Los
asistentes de etiquetas establecen el tipo de la propiedad TagMode después de leer una etiqueta.
ProcessAsync
En esta sección, escribiremos un asistente de correo electrónico asincrónico.
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 el asistente 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>");
}
}
}

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. Ejecutar 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". El asistente 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 el asistente 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 un asistente 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;
}
}
}

Como se ha indicado anteriormente, los asistentes de etiquetas convierten las propiedades y


nombres de clase de C# con grafía Pascal para asistentes de etiquetas en grafía kebab. 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 <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 el asistente 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";
WebsiteContext webContext = new WebsiteContext {
Version = new Version(1, 3),
CopyrightYear = 1638,
Approved = true,
TagsToShow = 131 };
}
<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="webContext" />

NOTE
En el marcado de Razor que se muestra a continuación:

<website-information info="webContext" />

Razor sabe que el atributo info es una clase, no una cadena, y usted quiere escribir código de C#. Todos los
atributos de asistentes 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 del
asistente de etiquetas:

<website-information info="webContext" >


</website-information>

Asistente de etiquetas de condición


El asistente 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 el
asistente 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 asistentes de etiquetas


En esta sección, escribirá un par de asistentes 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 estos dos asistentes están estrechamente relacionados y tal vez las refactorice en el futuro, los
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 el asistente 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. Ejecutar 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 del asistente de etiquetas
HTTP se ejecuta primero. El problema es que la salida del asistente de etiquetas se almacena en caché y,
cuando se ejecuta el asistente de etiquetas WWW, sobrescribe la salida almacenada en caché desdel
asistente de etiquetas HTTP. Más adelante en el tutorial veremos cómo se controla el orden en el que se
ejecutan los asistentes 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ó los asistentes 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 el último asistente 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 nuestro asistente de etiquetas de vinculación automática es correcta y está completa, tiene un
pequeño problema. Si el asistente 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 los
demás asistentes 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 el asistente de etiquetas HTTP se ejecuta antes que el asistente de
etiquetas WWW. Cambie Order a MaxValue y compruebe que el marcado generado para la etiqueta
WWW es incorrecto.

Inspeccionar y recuperar contenido secundario


Los asistentes 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é.

Carga de la vista parcial minimizada TagHelper


En entornos de producción, se puede mejorar el rendimiento cargando vistas parciales minimizadas. Para
aprovechar las ventajas de la vista parcial minimizada en producción, siga estos pasos:
Cree o configure un proceso de compilación anterior que minimice las vistas parciales.
Utilice el siguiente código para cargar las vistas parciales minimizadas en entornos que no son de desarrollo.
public class MinifiedVersionPartialTagHelper : PartialTagHelper
{
public MinifiedVersionPartialTagHelper(ICompositeViewEngine viewEngine,
IViewBufferScope viewBufferScope)
: base(viewEngine, viewBufferScope)
{

public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)


{
// Append ".min" to load the minified partial view.
if (!IsDevelopment())
{
Name += ".min";
}

return base.ProcessAsync(context, output);


}

private bool IsDevelopment()


{
return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
== EnvironmentName.Development;
}
}
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Componentes del asistente de etiquetas en ASP.NET
Core
18/06/2019 • 10 minutes to read • Edit Online

Por Scott Addie y Fiyaz Bin Hasan


El componente de un asistente de etiquetas es un asistente de etiquetas que permite modificar o agregar con
condiciones elementos HTML a partir del código del lado servidor. Esta característica está disponible en ASP.NET
Core 2.0 o versiones posteriores.
ASP.NET Core incluye dos componentes de asistente de etiquetas integrados: head y body . Se encuentran en el
espacio de nombres Microsoft.AspNetCore.Mvc.Razor.TagHelpers y pueden usarse tanto en MVC como en Razor
Pages. Los componentes de asistente de etiquetas no requieren el registro en la aplicación en _ViewImports.cshtml.
Vea o descargue el código de ejemplo (cómo descargarlo)

Casos de uso
Dos casos de uso comunes de componentes de asistente de etiquetas incluyen:
1. Insertar <link> en <head> .
2. Insertar <script> en <body> .

En las secciones siguientes se describen estos casos de uso.


Insertar en el elemento de encabezado HTML
Dentro del elemento <head> HTML, los archivos CSS suelen importarse con el elemento <link> HTML. El
código siguiente inserta un elemento <link> en el elemento <head> con el componente de asistente de etiquetas
head :
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
public class AddressStyleTagHelperComponent : TagHelperComponent
{
private readonly string _style =
@"<link rel=""stylesheet"" href=""/css/address.css"" />";

public override int Order => 1;

public override Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "head",
StringComparison.OrdinalIgnoreCase))
{
output.PostContent.AppendHtml(_style);
}

return Task.CompletedTask;
}
}
}

En el código anterior:
AddressStyleTagHelperComponent implementa TagHelperComponent. La abstracción:
Permite la inicialización de la clase con TagHelperContext.
Permite usar componentes de asistente de etiquetas para agregar o modificar elementos HTML.
La propiedad Order define el orden en el que se representan los componentes. Order es necesario cuando hay
varios usos de los componentes de asistente de etiquetas en una aplicación.
ProcessAsync compara el valor de propiedad TagName del contexto de ejecución con head . Si la comparación
se evalúa como true, el contenido del campo _style se inserta en el elemento <head> HTML.
Insertar en el elemento del cuerpo HTML
El componente de asistente de etiquetas body puede insertar un elemento <script> en el elemento <body> . El
código siguiente demuestra esta técnica:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
public class AddressScriptTagHelperComponent : TagHelperComponent
{
public override int Order => 2;

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "body",
StringComparison.OrdinalIgnoreCase))
{
var script = await File.ReadAllTextAsync(
"TagHelpers/Templates/AddressToolTipScript.html");
output.PostContent.AppendHtml(script);
}
}
}
}

Se usa un archivo HTML independiente para almacenar el elemento <script> . El archivo HTML hace que el
código sea más limpio y más fácil de mantener. El código anterior lee el contenido de
TagHelpers/Templates/AddressToolTipScript.html y lo anexa con la salida del asistente de etiquetas. El archivo
AddressToolTipScript.html incluye el siguiente marcado:

<script>
$("address[printable]").hover(function() {
$(this).attr({
"data-toggle": "tooltip",
"data-placement": "right",
"title": "Home of Microsoft!"
});
});
</script>

El código anterior enlaza un widget de información sobre herramientas de arranque a cualquier elemento
<address> que incluye un atributo printable . El efecto es visible cuando el puntero del mouse se sitúa sobre el
elemento.

Registro de un componente
Se debe agregar un componente de asistente de etiquetas a la colección de componentes de asistente de etiquetas
de la aplicación. Hay tres maneras de agregarlo a la colección:
Componentes del asistente de etiquetas en ASP.NET Core
Casos de uso
Insertar en el elemento de encabezado HTML
Insertar en el elemento del cuerpo HTML
Registro de un componente
Registro mediante el contenedor de servicios
Registro mediante un archivo de Razor
Registro con el modelo de página o controlador
Creación de un componente
Recursos adicionales
Registro mediante el contenedor de servicios
Si la clase de componente de asistente de etiquetas no se administran con ITagHelperComponentManager, se
debe registrar con el sistema de inserción de dependencias (DI). El código Startup.ConfigureServices siguiente
registra las clases AddressStyleTagHelperComponent y AddressScriptTagHelperComponent con una duración transitoria:

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.AddTransient<ITagHelperComponent,
AddressScriptTagHelperComponent>();
services.AddTransient<ITagHelperComponent,
AddressStyleTagHelperComponent>();
}

Registro mediante un archivo de Razor


Si el componente de asistente de etiquetas no está registrado con la inserción de dependencias, se puede registrar
desde una página de Razor Pages o desde una vista de MVC. Esta técnica se usa para controlar el orden de
ejecución del componente y el marcado insertado desde un archivo de Razor.
ITagHelperComponentManager se usa para agregar componentes de asistente de etiquetas o para quitarlos de la
aplicación. El código siguiente demuestra esta técnica con AddressTagHelperComponent :

@using RazorPagesSample.TagHelpers;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager manager;

@{
string markup;

if (Model.IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}

manager.Components.Add(new AddressTagHelperComponent(markup, 1));


}

En el código anterior:
La directiva @inject proporciona una instancia de ITagHelperComponentManager . La instancia está asignada a
una variable denominada manager para el acceso descendente en el archivo de Razor.
Se agrega una instancia de AddressTagHelperComponent a la colección de componentes de asistente de etiquetas
de la aplicación.
AddressTagHelperComponent se modifica para alojar un constructor que acepta los parámetros markup y order :
private readonly string _markup;

public override int Order { get; }

public AddressTagHelperComponent(string markup = "", int order = 1)


{
_markup = markup;
Order = order;
}

El parámetro markup proporcionado se utiliza en ProcessAsync como sigue:

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "address",
StringComparison.OrdinalIgnoreCase) &&
output.Attributes.ContainsName("printable"))
{
TagHelperContent childContent = await output.GetChildContentAsync();
string content = childContent.GetContent();
output.Content.SetHtmlContent(
$"<div>{content}<br>{_markup}</div>{_printableButton}");
}
}

Registro con el modelo de página o controlador


Si el componente de asistente de etiquetas no está registrado con la inserción de dependencias, se puede registrar
desde un modelo de página de Razor Pages o desde un controlador de MVC. Esta técnica es útil para separar la
lógica de C# de archivos de Razor.
La inserción del constructor se utiliza para acceder a una instancia de ITagHelperComponentManager . El componente
de asistente de etiquetas se agrega a la colección de componentes de asistente de etiquetas de la instancia. El
modelo de página de Razor Pages siguiente muestra esta técnica con AddressTagHelperComponent :
using System;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesSample.TagHelpers;

public class IndexModel : PageModel


{
private readonly ITagHelperComponentManager _tagHelperComponentManager;

public bool IsWeekend


{
get
{
var dayOfWeek = DateTime.Now.DayOfWeek;

return dayOfWeek == DayOfWeek.Saturday ||


dayOfWeek == DayOfWeek.Sunday;
}
}

public IndexModel(ITagHelperComponentManager tagHelperComponentManager)


{
_tagHelperComponentManager = tagHelperComponentManager;
}

public void OnGet()


{
string markup;

if (IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}

_tagHelperComponentManager.Components.Add(
new AddressTagHelperComponent(markup, 1));
}
}

En el código anterior:
La inserción del constructor se utiliza para acceder a una instancia de ITagHelperComponentManager .
Se agrega una instancia de AddressTagHelperComponent a la colección de componentes de asistente de etiquetas
de la aplicación.

Creación de un componente
Para crear un componente de asistente de etiquetas personalizado:
Cree una clase pública derivada de TagHelperComponentTagHelper.
Aplique un atributo [HtmlTargetElement] a la clase. Especifique el nombre del elemento HTML de destino.
Opcional: aplique un atributo [EditorBrowsable(EditorBrowsableState.Never)] a la clase para suprimir la
presentación del tipo en IntelliSense.
El código siguiente crea un componente de asistente de etiquetas personalizado que tiene como destino el
elemento <address> HTML:
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace RazorPagesSample.TagHelpers
{
[HtmlTargetElement("address")]
[EditorBrowsable(EditorBrowsableState.Never)]
public class AddressTagHelperComponentTagHelper : TagHelperComponentTagHelper
{
public AddressTagHelperComponentTagHelper(
ITagHelperComponentManager componentManager,
ILoggerFactory loggerFactory) : base(componentManager, loggerFactory)
{
}
}
}

Use el componente de asistente de etiquetas address personalizado para insertar el marcado HTML como sigue:

public class AddressTagHelperComponent : TagHelperComponent


{
private readonly string _printableButton =
"<button type='button' class='btn btn-info' onclick=\"window.open("
"'https://binged.it/2AXRRYw')\">" +
"<span class='glyphicon glyphicon-road' aria-hidden='true'></span>" +
"</button>";

public override int Order => 3;

public override async Task ProcessAsync(TagHelperContext context,


TagHelperOutput output)
{
if (string.Equals(context.TagName, "address",
StringComparison.OrdinalIgnoreCase) &&
output.Attributes.ContainsName("printable"))
{
var content = await output.GetChildContentAsync();
output.Content.SetHtmlContent(
$"<div>{content.GetContent()}</div>{_printableButton}");
}
}
}

El método ProcessAsync anterior inserta el elemento HTML proporcionado a SetHtmlContent en el elemento


<address> coincidente. La inserción se produce cuando:
El valor de propiedad TagName del contexto de ejecución es igual a address .
El elemento <address> correspondiente tiene un atributo printable .
Por ejemplo, la instrucción if se evalúa como true al procesar el siguiente elemento <address> :

<address printable>
One Microsoft Way<br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
Recursos adicionales
Inserción de dependencias en ASP.NET Core
Inserción de dependencias en vistas de ASP.NET Core
Asistentes de etiquetas integradas de ASP.NET Core
Asistente de etiquetas delimitadoras en
ASP.NET Core
17/05/2019 • 12 minutes to read • Edit Online

De Peter Kellner y Scott Addie


El asistente 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- .
Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET
Core.
Vea o descargue el código de ejemplo (cómo descargarlo)
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; }
}

Atributos del asistente de etiquetas delimitadoras


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 el asistente 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: "mvcAreaRoute",
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>

El asistente 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. El
asistente 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 asp-area provoca una reasignación de rutas.
Uso en Razor Pages
Se admiten áreas de Razor Pages en ASP.NET Core 2.1 o versiones posteriores.
Tenga en cuenta la siguiente jerarquía de directorios:
{Nombre del proyecto}
wwwroot
Áreas
Sesiones
Páginas
_ViewStart.cshtml
Index.cshtml
Index.cshtml.cs
Páginas
El marcado para hacer referencia a la página Razor Índice del área Sesiones es:

<a asp-area="Sessions"
asp-page="/Index">View Sessions</a>

El código HTML generado:


<a href="/Sessions">View Sessions</a>

TIP
Para admitir áreas en una aplicación de Razor Pages, realice una de las siguientes acciones en
Startup.ConfigureServices :

Establezca la versión de compatibilidad en 2.1 o posterior.


Establezca la propiedad RazorPagesOptions.AllowAreas en true :

services.AddMvc()
.AddRazorPagesOptions(options => options.AllowAreas = true);

Uso en MVC
Tenga en cuenta la siguiente jerarquía de directorios:
{Nombre del proyecto}
wwwroot
Áreas
Blogs
Controladores
HomeController.cs
Vistas
Página principal
AboutBlog.cshtml
Index.cshtml
_ViewStart.cshtml
Controladores
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. El marcado para hacer referencia a la vista AboutBlog
es:

<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 admitir áreas 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:

app.UseMvc(routes =>
{
// need route and attribute on controller: [Area("Blogs")]
routes.MapRoute(name: "mvcAreaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}");

// default route for non-areas


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

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 en el ejemplo es localhost. El asistente de etiquetas delimitadoras utiliza 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-pagees 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 . Cuando el método es asincrónico, también se omite 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 de ASP.NET Core
Introducción a las páginas de Razor en ASP.NET Core
Versión de compatibilidad para ASP.NET Core MVC
Asistente de etiquetas de caché en ASP.NET Core
MVC
10/05/2019 • 9 minutes to read • Edit Online

Por Peter Kellner y Luke Latham


El asistente de etiquetas de caché proporciona la capacidad para mejorar 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.
Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.
Este marcado de Razor almacena en caché la fecha actual:

<cache>@DateTime.Now</cache>

La primera solicitud a la página que contiene el asistente de etiquetas muestra la fecha actual. Las solicitudes
adicionales muestran el valor almacenado en caché hasta que la memoria caché expira (el valor predeterminado
es 20 minutos) o hasta que la fecha almacenada en caché se expulsa de la memoria caché.

Atributos del asistente de etiqueta de caché


enabled
TIPO DE ATRIBUTO EJEMPLOS DEFAULT

Booleano true , false true

enabled determina si se almacena en caché el contenido incluido por el asistente de etiquetas de caché. De
manera predeterminada, es true . Si establece en false , el resultado representado no se almacena en caché.
Ejemplo:

<cache enabled="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

expires-on
TIPO DE ATRIBUTO EJEMPLO

DateTimeOffset @new DateTime(2025,1,29,17,02,0)

expires-on establece una fecha de expiración absoluta para el elemento almacenado en caché.
En este ejemplo se almacena en caché el contenido del asistente de etiquetas de caché hasta las 17:02 del 29 de
enero de 2025.
<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 EJEMPLO DEFAULT

TimeSpan @TimeSpan.FromSeconds(120) 20 minutos

expires-after establece el período de tiempo desde el momento de 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>

El motor de vistas de Razor establece el valor predeterminado expires-after en veinte minutos.


expires-sliding
TIPO DE ATRIBUTO EJEMPLO

TimeSpan @TimeSpan.FromSeconds(60)

Establece la hora en que se debe expulsar una entrada de caché si no se ha accedido a su valor.
Ejemplo:

<cache expires-sliding="@TimeSpan.FromSeconds(60)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-header
TIPO DE ATRIBUTO EJEMPLOS

String User-Agent , User-Agent,content-encoding

vary-by-header acepta una lista de los valores de encabezado separados por comas 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 almacena en caché el
contenido de cada User-Agent diferente que se presenta al servidor web:

<cache vary-by-header="User-Agent">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-query
TIPO DE ATRIBUTO EJEMPLOS

String Make , Make,Model

vary-by-queryacepta una lista de valores separados por comas de Keys en una cadena de consulta (Query) que
desencadenan una actualización de la caché cuando cambia el valor de cualquiera de las claves.
En este ejemplo se supervisan los valores de Make y Model . En el ejemplo se almacena en caché el contenido
de cada Make y Model diferente que se presenta al servidor web:

<cache vary-by-query="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>

vary-by-route
TIPO DE ATRIBUTO EJEMPLOS

String Make , Make,Model

vary-by-route acepta una lista delimitada por comas de nombres de parámetros de ruta que desencadenan una
actualización de la caché cuando el valor del parámetro de datos de ruta cambia.
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 EJEMPLOS

String .AspNetCore.Identity.Application ,
.AspNetCore.Identity.Application,HairColor

vary-by-cookie acepta una lista delimitada por comas de nombres de cookies que desencadenan una
actualización de la caché cuando los valores de las cookies cambian.
En este ejemplo se supervisa la cookie asociada con ASP.NET Core Identity. Cuando se autentica un usuario, un
cambio en la cookie de identidad desencadena una actualización de caché:

<cache vary-by-cookie=".AspNetCore.Identity.Application">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-user
TIPO DE ATRIBUTO EJEMPLOS DEFAULT

Booleano true , false true

vary-by-user especifica la memoria caché se restablece o no cuando cambia el usuario que ha iniciado la sesión
(o la entidad de seguridad del contexto). 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 este ejemplo se supervisa el usuario actual que ha iniciado sesión para desencadenar una actualización de
caché:

<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. Cuando el
valor se establece en true , un ciclo de autenticación invalida la memoria caché para el usuario autenticado. Se
invalida la memoria caché porque se genera un nuevo valor único de cookie cuando se autentica un usuario. Se
mantiene la memoria caché para el estado anónimo cuando no se presenta ninguna cookie o la cookie ha
expirado. Si no se autentica el usuario, se mantiene la memoria caché.
vary-by
TIPO DE ATRIBUTO EJEMPLO

String @Model

vary-by permite personalizar qué datos se almacenan en caché. Cuando el objeto al que hace referencia el
valor de cadena del atributo cambia, el contenido del asistente de etiqueta de caché se actualiza. A menudo se
asignan a este atributo una concatenación de cadenas de valores del modelo. De hecho, esto provoca una
situación en la que una actualización de cualquiera de los valores concatenados invalida la memoria caché.
En este ejemplo 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 de la suma como la propiedad de
modelo simple. Cuando se cambia esta suma, el contenido del asistente de etiqueta de caché se representa y
almacena en caché de nuevo.
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 EJEMPLOS DEFAULT

CacheItemPriority High , Low , NeverRemove , Normal Normal

priority proporciona instrucciones de expulsión de caché para el proveedor de caché integrado. El servidor
web expulsa 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 conserven los
elementos en la memoria caché. Para más información, eche un vistazo a los temas de la sección Recursos
adicionales.
El asistente de etiquetas de caché es dependiente del servicio de caché de memoria. El asistente de etiquetas de
caché agrega el servicio si no se ha agregado.

Recursos adicionales
Almacenar en caché en memoria en ASP.NET Core
Introducción a la identidad en ASP.NET Core
Asistente de etiquetas de caché distribuida en
ASP.NET Core
10/05/2019 • 3 minutes to read • Edit Online

Por Peter Kellner y Luke Latham


El asistente 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.
Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.
El asistente de etiquetas de caché distribuida hereda de la misma clase base que el asistente de etiquetas de
caché. Todos los atributos del asistente de etiquetas de caché están disponibles para el asistente de etiquetas
distribuidas.
El asistente de etiquetas de caché distribuida usa la inserción de constructor. La interfaz IDistributedCache se
pasa al constructor del asistente de etiquetas de caché distribuida. Si no se ha creado ninguna implementación
específica de IDistributedCache en Startup.ConfigureServices (Startup.cs), el asistente de etiquetas de caché
distribuida usa el mismo proveedor en memoria para almacenar datos en caché como el asistente de etiquetas
de caché.

Atributos del asistente de etiquetas de caché distribuida


Atributos compartidos con el asistente de etiquetas de caché
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

El asistente de etiquetas de caché distribuida hereda de la misma clase que el asistente de etiquetas de caché.
Para obtener descripciones de estos atributos, vea el asistente de etiquetas de caché.
name
TIPO DE ATRIBUTO EJEMPLO

String my-distributed-cache-unique-key-101

name es obligatorio. El atributo name se usa como clave para cada instancia de caché almacenada. A diferencia
del asistente de etiquetas de caché, que asigna una clave de caché a cada instancia en función del nombre de la
página de Razor y la ubicación en la página de Razor, el asistente de etiquetas de caché distribuida solo basa su
clave en el atributo name .
Ejemplo:

<distributed-cache name="my-distributed-cache-unique-key-101">
Time Inside Cache Tag Helper: @DateTime.Now
</distributed-cache>

Implementaciones de IDistributedCache del asistente 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 Almacenamiento en caché en ASP.NET Core distribuido encontrará detalles de estas
implementaciones. Para ambas implementaciones hay que establecer una instancia de IDistributedCache en
Startup .

No hay atributos de etiqueta asociados específicamente con el uso de implementaciones concretas de


IDistributedCache .

Recursos adicionales
Asistente de etiquetas de caché en ASP.NET Core MVC
Inserción de dependencias en ASP.NET Core
Almacenamiento en caché en ASP.NET Core distribuido
Almacenar en caché en memoria en ASP.NET Core
Introducción a la identidad en ASP.NET Core
Asistente de etiquetas de entorno en ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

Por Peter Kellner, Hisham Bin Ateya y Luke Latham


El asistente de etiquetas de entorno representa condicionalmente el contenido incluido en función del entorno de
hospedaje actual. El único atributo del asistente de etiquetas de entorno, names , es una lista de nombres de
entorno separados por comas. Si alguno de los nombres de entorno proporcionados coincide con el entorno
actual, se representa el contenido incluido.
Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.

Atributos del asistente de etiquetas de entorno


nombres
names acepta un solo nombre de entorno de hospedaje o una lista de nombres de entorno de hospedaje
separados por comas que desencadenan la representación del contenido incluido.
Los valores de entorno se comparan con el valor actual devuelto por IHostingEnvironment.EnvironmentName.
La comparación ignora el uso de mayúsculas y minúsculas.
En este ejemplo se usa un asistente de etiquetas de entorno. El contenido se representa si el entorno de
hospedaje es de almacenamiento provisional o de producción:

<environment names="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>

Atributos include y exclude


Los atributos include & exclude controlan la representación del contenido incluido en función de los nombres
de entorno de hospedaje incluidos o excluidos.
include
La propiedad include exhibe un comportamiento similar al atributo names . Un entorno que se muestra en el
valor de atributo include debe coincidir con el entorno de hospedaje de la aplicación
(IHostingEnvironment.EnvironmentName) para representar el contenido de la etiqueta <environment> .

<environment include="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>

exclude
A diferencia del atributo include , el contenido de la etiqueta <environment> se representa cuando el entorno de
hospedaje no coincide con un entorno que se muestra en el valor de atributo exclude .

<environment exclude="Development">
<strong>HostingEnvironment.EnvironmentName is not Development</strong>
</environment>
Recursos adicionales
Usar varios entornos en ASP.NET Core
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistente de etiquetas de imagen en ASP.NET Core
10/05/2019 • 3 minutes to read • Edit Online

Por Peter Kellner


El asistente de etiquetas de imagen mejora la etiqueta <img> para proporcionar un comportamiento de limpieza
de caché para archivos de imagen estática.
Una cadena de limpieza de memoria caché es un valor único que representa el valor hash del archivo de imagen
estático anexados a la URL del activo. La cadena única pide a los clientes (y algunos servidores proxy) que vuelva
a cargar la imagen desde el servidor web del host y no desde la memoria caché del cliente.
Si el origen de la imagen ( src ) es un archivo estático en el servidor web del host:
Se anexa una cadena única de limpieza de caché como un parámetro de consulta al origen de la imagen.
Si el archivo en el servidor web del host cambia, se genera una dirección URL de solicitud única que incluye el
parámetro de solicitud actualizada.
Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.

Atributos del asistente de etiquetas de imagen


src
Para activar el asistente de etiquetas de imagen, se necesita el atributo src en el elemento <img> .
El origen de la imagen ( src ) debe apuntar a un archivo estático físico en el servidor. Si src es un identificador
URI remoto, el parámetro de cadena de consulta de limpieza de caché no se genera.
asp-append-version
Cuando se especifica asp-append-version con un valor true junto con un atributo src , se invoca el asistente de
etiquetas de imagen.
En este ejemplo se usa un asistente de etiquetas de imagen:

<img src="~/images/asplogo.png" asp-append-version="true">

Si el archivo estático existe en el directorio /wwwroot/images/, 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 /wwwroot/images/ almacenado en disco. Si el
servidor web no es capaz de tener acceso de lectura al archivo estático, no se agregará ningún parámetro v al
atributo src en el marcado representado.

Comportamiento de almacenamiento en caché de hash


El asistente de etiquetas de imagen usa el proveedor de caché en el servidor web local para almacenar el hash
Sha512 calculado de un archivo determinado. Si se solicita el archivo varias veces, no se vuelven a calcular el
hash. La memoria caché queda invalidada por un monitor del archivo que se adjunta al archivo cuando se calcula
el hash Sha512 del archivo. Cuando el archivo se cambia en el disco, se calcula un nuevo hash y se almacena en
caché.

Recursos adicionales
Almacenar en caché en memoria en ASP.NET Core
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistente de etiquetas parciales en ASP.NET Core
10/05/2019 • 5 minutes to read • Edit Online

Por Scott Addie


Para obtener información general sobre asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.
Vea o descargue el código de ejemplo (cómo descargarlo)

Información general
El asistente 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 del asistente de HTML.
Presenta la vista parcial de forma asincrónica.
Las opciones del asistente de 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 del asistente 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 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 un asistente de HTML


Tenga en cuenta el siguiente ejemplo del asistente de HTML asincrónico. 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)
}

El siguiente asistente de etiquetas parciales logra el mismo comportamiento de representación asincrónica que
el asistente de 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
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistentes de etiquetas en formularios de ASP.NET
Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los
asistentes 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 los asistentes de etiquetas) antes de este documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un asistente de etiquetas
específico, pero es importante tener en cuenta que los asistentes de etiquetas no reemplazan a los asistentes de
HTML y que no hay un asistente de etiquetas para cada asistente de HTML. Si existe una alternativa del asistente
de HTML, se mencionará aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente de etiquetas Form
asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la etiqueta <button ...> o
<input type="image" ...> generadas. El atributo formaction controla el lugar donde un formulario envía sus datos.
Enlaza con los elementos <input> de tipo image y los elementos <button>. El asistente de etiquetas de acción de
formulario permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo formaction se
genera para el elemento correspondiente.
Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por ejemplo:


asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.


Ejemplo de envío al controlador
El marcado siguiente envía el formulario a la acción Index de HomeController cuando se selecciona el elemento
input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :

public class HomeController : Controller


{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:


<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y Html.EditorFor . Vea la
sección Alternativas del asistente de HTML al asistente de etiquetas Input para obtener más
información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se actualiza el asistente 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?)

El asistente 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-local"
TIPO DE .NET TIPO DE ENTRADA

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 el asistente 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. El
asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se superponen al
asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a asp-for o


Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo permite), ya que no necesita asignar
ningún enumerador; sin embargo, la evaluación de un indizador en una expresión LINQ puede resultar caro y, por
tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>

Asistente de etiquetas Label


Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el identificador asociado al
elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación muestra estos mensajes de error
cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo asp-validation-summary .
Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de 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 Index()


{
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 el asistente 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
del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Asistentes de etiquetas en formularios de
ASP.NET Core
10/05/2019 • 32 minutes to read • Edit Online

Por Rick Anderson, N. Taylor Mullen, 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 los asistentes 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 los asistentes de etiquetas) antes de este
documento.
En muchos casos, los asistentes de HTML proporcionan un método alternativo para un
asistente de etiquetas específico, pero es importante tener en cuenta que los asistentes de
etiquetas no reemplazan a los asistentes de HTML y que no hay un asistente de etiquetas
para cada asistente de HTML. Si existe una alternativa del asistente de HTML, se mencionará
aquí.

Asistente de etiquetas de formulario (Form)


El asistente 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 del asistente de


HTML.
Ejemplo:

<form asp-controller="Demo" asp-action="Register" method="post">


<!-- Input and Submit elements -->
</form>

El asistente 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 del asistente
de etiquetas Form asp-controller y asp-action . El asistente 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 el asistente de etiquetas Form presta este servicio.
Uso de una ruta con nombre
El atributo del asistente 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.

Asistente de etiquetas de acción de formulario


El asistente de etiquetas de acción de formulario genera el atributo formaction en la
etiqueta <button ...> o <input type="image" ...> generadas. El atributo formaction
controla el lugar donde un formulario envía sus datos. Enlaza con los elementos <input> de
tipo image y los elementos <button>. El asistente de etiquetas de acción de formulario
permite el uso de varios atributos AnchorTagHelper asp- para controlar qué vínculo
formaction se genera para el elemento correspondiente.

Atributos AnchorTagHelper admitidos para controlar el valor de formaction :

ATRIBUTO DESCRIPCIÓN

asp-controller El nombre del controlador.

asp-action El nombre del método de acción.

asp-area El nombre del área.

asp-page El nombre de la página de Razor.


ATRIBUTO DESCRIPCIÓN

asp-page-handler El nombre del controlador de páginas de Razor.

asp-route El nombre de la ruta.

asp-route-{value} Un valor único de ruta de dirección URL. Por


ejemplo: asp-route-id="1234" .

asp-all-route-data Todos los valores de ruta.

asp-fragment El fragmento de dirección URL.

Ejemplo de envío al controlador


El marcado siguiente envía el formulario a la acción Index de HomeController cuando se
selecciona el elemento input o button:

<form method="post">
<button asp-controller="Home" asp-action="Index">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-controller="Home"
asp-action="Index">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>

Ejemplo de envío a la página


El marcado siguiente envía el formulario a la página de Razor About :

<form method="post">
<button asp-page="About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/About">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>

Ejemplo de envío a la ruta


Tenga en cuenta el punto de conexión /Home/Test :
public class HomeController : Controller
{
[Route("/Home/Test", Name = "Custom")]
public string Test()
{
return "This is the test page";
}
}

El marcado siguiente envía el formulario al punto de conexión /Home/Test .

<form method="post">
<button asp-route="Custom">Click Me</button>
<input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>

El marcado anterior genera el siguiente código HTML:

<form method="post">
<button formaction="/Home/Test">Click Me</button>
<input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>

Asistente de etiquetas de entrada (Input)


El asistente 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>">

El asistente 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 del asistente de HTML que se superponen a Html.TextBoxFor y
Html.EditorFor . Vea la sección Alternativas del asistente de HTML al asistente
de etiquetas Input para obtener más información.
Permite establecer tipado fuerte. Si el nombre de la propiedad cambia y no se
actualiza el asistente 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?)

El asistente 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-local"

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 el


asistente 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. El asistente 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 del asistente de HTML al asistente de etiquetas Input
Html.TextBox , Html.TextBoxFor , Html.Editor y Html.EditorFor tienen características que se
superponen al asistente de etiquetas Input. El asistente 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 el asistente de etiquetas Input no. El asistente 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 usan una entrada ViewDataDictionary especial
@Html.EditorFor()
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 asistentes de etiquetas 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>
*@

Si es posible, debe usarse foreach si el valor se va a utilizar en un contexto equivalente a


asp-for o Html.DisplayFor . En general, for es mejor que foreach (si el escenario lo
permite), ya que no necesita asignar ningún enumerador; sin embargo, la evaluación de un
indizador en una expresión LINQ puede resultar caro y, por tanto, se debe minimizar.

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.

Asistente de etiquetas de área de texto (Textarea)


El asistente de etiquetas Textarea Tag Helper es similar al asistente 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 del asistente de 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>
Asistente de etiquetas Label
Genera el título de la etiqueta y el atributo for en un elemento <etiqueta> de un
nombre de expresión.
Alternativa del asistente de 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 el asistente 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>

El asistente de etiquetas Label genera el valor de atributo for de "Email", que es el


identificador asociado al elemento <input> . Los asistentes de etiquetas 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.

Asistentes de etiquetas de validación


Hay dos asistentes 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. El asistente de etiquetas de validación
muestra estos mensajes de error cuando se produce un error de validación.
Asistente 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 del asistente de 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>

El asistente 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 un asistente 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>

Asistente de etiquetas de resumen de validación


Tiene como destino todos los elementos <div> con el atributo
asp-validation-summary .

Alternativa del asistente de 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, el asistente 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>

Asistente 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 del asistente de
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 Index()
{
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 el asistente 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 del asistente 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
El asistente 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
Asistentes de etiquetas en ASP.NET Core
HTML Form element (Elemento HTML Form)
Request Verification Token (Token de comprobación de solicitudes)
Enlace de modelos en ASP.NET Core
Validación de modelos en ASP.NET Core MVC
IAttributeAdapter Interface (Interfaz IAttributeAdapter)
Fragmentos de código de este documento
Componentes de vista en ASP.NET Core
21/05/2019 • 21 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ó usando controladores y vistas, pero los componentes de vista también funcionan con
Razor Pages.
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 .
Al estudiar si los componentes de visualización cumplen las especificaciones de una aplicación, considere la
posibilidad de usar los componentes de Razor en su lugar. Los componentes de Razor también combinan el
marcado con código de C# para producir unidades de interfaz de usuario reutilizables. Los componentes de
Razor están diseñados para ofrecer productividad a los desarrolladores mediante elementos de composición y
lógica de interfaz de usuario del lado cliente. Para obtener más información, vea Create and use Razor
components.

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 un elemento
Task<IViewComponentResult> o en un método Invoke sincrónico que devuelve un elemento
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:
Defina un método InvokeAsync que devuelva un elemento Task<IViewComponentResult> o un método
Invoke sincrónico que devuelva un elemento IViewComponentResult .
Por lo general, inicializa un modelo y lo pasa 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. No hay ningún enlace de
modelos.
No son accesibles directamente como punto de conexión HTTP. Se invocan desde el código (normalmente
en una vista). Un componente de vista nunca controla una solicitud.
Se sobrecargan en la firma, en lugar de los detalles de la solicitud HTTP actual.
Ruta de búsqueda de la vista
El tiempo de ejecución busca la vista en las rutas de acceso siguientes:
/Views/{Controller Name}/Components/{Nombre de componente de vista}/{Nombre de vista}
/Views/Shared/Components/{Nombre de componente de vista}/{Nombre de vista}
/Pages/Shared/Components/{Nombre de componente de vista}/{Nombre de vista}
La ruta de búsqueda se aplica a los proyectos que utilizan controladores y vistas y Razor Pages.
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 el nombre Default.cshtml al archivo de vista y use la ruta de acceso
Views/Shared/Components/{Nombre de 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:

@await 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 un asistente de


etiquetas
Para ASP.NET Core 1.1 y versiones posteriores, puede invocar un componente de vista como un asistente 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 los asistentes de etiquetas se convierten a su grafía
kebab. El asistente 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]>

Para usar un componente de vista como un asistente de etiquetas, registre el ensamblado que contiene el
componente de vista mediante la directiva @addTagHelper . 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 un asistente de etiquetas en cualquier archivo que haga
referencia al componente de vista. Vea Administración del ámbito de los asistentes de etiquetas para más
información sobre cómo registrar asistentes de etiquetas.
El método InvokeAsync usado en este tutorial:

@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })

En el marcado del asistente 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.
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:

@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h3>Priority Items</h3>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>

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/ToDo/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 codificadas de forma rígida
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>

@*
Note:
To use the below line, you need to #define no_suffix in ViewComponents/PriorityList.cs or it
won't compile.
By doing so it will cause a problem to index as there will be mutliple viewcomponents
with the same name after the compiler removes the suffix "ViewComponent"
*@

@*@await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })*@


</div>

Realizar el trabajo sincrónico


El marco controla la invocación de un método Invoke sincrónico si no necesita realizar un trabajo asincrónico.
El método siguiente crea un componente de vista Invoke sincrónico:

public class PriorityList : ViewComponent


{
public IViewComponentResult Invoke(int maxPriority, bool isDone)
{
var items = new List<string> { $"maxPriority: {maxPriority}", $"isDone: {isDone}" };
return View(items);
}
}

El archivo de Razor del componente de vista enumera las cadenas pasadas al método Invoke
(Views/Home/Components/PriorityList/Default.cshtml):

@model List<string>

<h3>Priority Items</h3>
<ul>
@foreach (var item in Model)
{
<li>@item</li>
}
</ul>

Se invoca el componente de vista en un archivo de Razor (por ejemplo, Views/Home/Index.cshtml) con uno de
los siguientes métodos:
IViewComponentHelper
Asistente de etiquetas
Para usar el método IViewComponentHelper, llame a Component.InvokeAsync :
Se invoca el componente de vista en un archivo de Razor (por ejemplo, Views/Home/Index.cshtml) con
IViewComponentHelper.
Llame a Component.InvokeAsync :

@await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })

Para usar el asistente de etiquetas, registre el ensamblado que contiene el componente de vista con el uso de
la directiva @addTagHelper (el componente de vista se encuentra en un ensamblado denominado MyWebApp ):

@addTagHelper *, MyWebApp

Use el asistente de etiquetas del componente de vista en el archivo de marcado de Razor:

<vc:priority-list max-priority="999" is-done="false">


</vc:priority-list>

La firma del método de PriorityList.Invoke es sincrónica, pero Razor busca y llama al método con
Component.InvokeAsync en el archivo de marcado.

Recursos adicionales
Inserción de dependencias en vistas
Compilación de archivos de Razor en ASP.NET Core
02/07/2019 • 6 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.
Los archivos de Razor se compilan en tiempo de compilación y publicación mediante el SDK de Razor. La
compilación en tiempo de ejecución se puede habilitar opcionalmente mediante la configuración de la aplicación

Compilación de Razor
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. Cuando se habilita la compilación en tiempo de ejecución, complementa a la
de tiempo de compilación y permite que se actualicen los archivos de Razor si se modifican.
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 implementan con la aplicación los archivos Views.dll
y los que no son .cshtml, o las referencias de los ensamblados necesarios para compilar los archivos de Razor.

IMPORTANT
La herramienta de precompilación está en desuso y 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 está en desuso y 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_del_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:

Compilación en tiempo de ejecución


La compilación en tiempo de compilación se complementa con la compilación en tiempo de ejecución de archivos
de Razor. ASP.NET Core MVC volverá a compilar los archivos de Razor cuando cambie el contenido de un
archivo .cshtml.
La compilación en tiempo de compilación se complementa con la compilación en tiempo de ejecución de archivos
de Razor. RazorViewEngineOptions AllowRecompilingViewsOnFileChange obtiene o establece un valor que
determina si los archivos de Razor (vistas y Razor Pages) se vuelven a compilar y actualizar si cambian los
archivos en el disco.
El valor predeterminado es true para:
Si la versión de compatibilidad de la aplicación se establece en Version_2_1 o una versión anterior
Si la versión de compatibilidad de la aplicación se establece en Version_2_2 o una versión posterior, y la
aplicación está en el entorno de desarrollo IsDevelopment. En otras palabras, los archivos de Razor no se
vuelven a compilar en el entorno que no es de desarrollo a menos que AllowRecompilingViewsOnFileChange
se establezca de forma explícita.
Para obtener instrucciones y ejemplos de configuración de la versión de compatibilidad de la aplicación, consulte
Versión de compatibilidad para ASP.NET Core MVC.
La compilación en tiempo de ejecución se habilita mediante el paquete
Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation . Para habilitar la compilación en tiempo de ejecución, las
aplicaciones deben:
Instalar el paquete NuGet Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.
Actualice el método Startup.ConfigureServices del proyecto para incluir una llamada a
AddRazorRuntimeCompilation :

services
.AddControllersWithViews()
.AddRazorRuntimeCompilation();

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
Trabajar con el modelo de aplicación en ASP.NET
Core
10/05/2019 • 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
30/05/2019 • 40 minutes to read • Edit Online

Por Kirk Larkin, Rick Anderson, Tom Dykstra y Steve Smith


Los filtros en ASP.NET Core permiten que se ejecute el código antes o después de determinadas fases de la
canalización del procesamiento de la solicitud.
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).
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. Entre los ejemplos de
cuestiones transversales se incluyen el control de errores, el almacenamiento en caché, la configuración, la
autorización y el registro. Los filtros evitan la duplicación de código. Así, por ejemplo, un filtro de excepción
de control de errores puede consolidar el control de errores.
Este documento se aplica a Razor Pages, a los controladores de API y a los controladores con vistas.
Vea o descargue el ejemplo (cómo descargarlo).

Funcionamiento de los filtros


Los filtros se ejecutan dentro de la canalización de invocación de acciones de ASP.NET Core, a veces
denominada canalización de filtro. La canalización de filtro se ejecuta después de que ASP.NET Core
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 está
autorizado para realizar la solicitud. Los filtros de autorización pueden cortocircuitar la canalización si
una solicitud no está autorizada.
Filtros de recursos:
Se ejecutan después de la autorización.
OnResourceExecuting puede ejecutar código antes que el resto de la canalización del filtro. Por
ejemplo, OnResourceExecuting puede ejecutar código antes que el enlace de modelos.
OnResourceExecuted puede ejecutar el código una vez que el resto de la canalización se haya
completado.
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 acciones no se admiten en Razor Pages.
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 los 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 pueden ejecutar código antes ( On-Stage-Executing ) y después ( On-Stage-Executed ) de
la fase de canalización. Por ejemplo, OnActionExecuting se llama antes de llamar al método de acción.
OnActionExecuted se llama después de devolver el método de acción.

public class MySampleActionFilter : 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 método On-Stage-ExecutionAsync :

public class SampleAsyncActionFilter : IAsyncActionFilter


{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// Do something before the action executes.

// next() calls the action method.


var resultContext = await next();
// resultContext.Result is set.
// Do something after the action executes.
}
}

En el código anterior, SampleAsyncActionFilter tiene un delegado ActionExecutionDelegate ( next ) que


ejecuta el método de acción. Cada uno de los métodos On-Stage-ExecutionAsync toman un
FilterType-ExecutionDelegate que ejecuta la fase de canalización del filtro.
Varias fases de filtro
Se pueden implementar interfaces para varias fases de filtro en una sola clase. Por ejemplo, la clase
ActionFilterAttribute implementa IActionFilter , IResultFilter y sus equivalentes asincrónicos.
Implemente la versión sincrónica o la versión asincrónica de una interfaz de filtro, pero no ambas. El entorno
de ejecución comprueba primero si el filtro implementa la interfaz asincrónica y, si es así, llama a la interfaz.
De lo contrario, llamará a métodos de interfaz sincrónicos. Si se implementan las interfaces asincrónicas y
sincrónicas en una clase, solo se llama 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.
Atributos de filtros integrados
ASP.NET Core 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:

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. Aplique el
AddHeaderAttribute a un método de acción o controlador y especifique el nombre y el valor del encabezado
HTTP:

[AddHeader("Author", "Joe Smith")]


public class SampleController : Controller
{
public IActionResult Index()
{
return Content("Examine the headers using the F12 developer tools.");
}

[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header is set.");
}

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

Ámbitos del filtro y orden de ejecución


Un filtro se puede agregar a la canalización en uno de tres ámbitos posibles:
Mediante un atributo en una acción.
Mediante un atributo en un controlador.
Globalmente para todos los controladores y las acciones, tal como se muestra en el siguiente código:

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(MySampleActionFilter)); // By type
options.Filters.Add(new SampleGlobalActionFilter()); // An instance
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

El código anterior agrega tres filtros globalmente mediante la colección MvcOptions.Filters.


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.
Como resultado de este anidamiento de filtros, el código de filtros posterior se ejecuta en el orden inverso al
código anterior. La secuencia de filtro:
El código anterior de los filtros globales.
El código anterior de los filtros de controlador.
El código anterior de los filtros de métodos de acción.
El código posterior de los filtros de métodos de acción.
El código posterior de los filtros de controlador.
El código posterior de los filtros globales.
El ejemplo siguiente 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
SECUENCIA ÁMBITO DEL FILTRO MÉTODO DE FILTRO

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.
Filtros de nivel de controlador y de páginas de Razor
Cada controlador que hereda de la clase base Controller incluye los métodos Controller.OnActionExecuting,
Controller.OnActionExecutionAsync y Controller.OnActionExecuted OnActionExecuted . Estos métodos:
Encapsulan los filtros que se ejecutan para una acción determinada.
OnActionExecuting se llama antes de cualquiera de los filtros de acciones.
OnActionExecuted se llama después de todos los filtros de acciones.
OnActionExecutionAsync se llama antes de cualquiera de los filtros de acciones. El código del filtro después
de next se ejecuta después del método de acción.

Por ejemplo, en el ejemplo de descarga, se aplica MySampleActionFilter globalmente al inicio.


El TestController :
Aplica SampleActionFilterAttribute ( [SampleActionFilter] ) a la acción FilterTest2 :
Invalida OnActionExecuting y OnActionExecuted .

public class TestController : Controller


{
[SampleActionFilter]
public IActionResult FilterTest2()
{
return Content($"From FilterTest2");
}

public override void OnActionExecuting(ActionExecutingContext context)


{
// Do something before the action executes.
base.OnActionExecuting(context);
}

public override void OnActionExecuted(ActionExecutedContext context)


{
// Do something after the action executes.
base.OnActionExecuted(context);
}
}

Si se dirige a https://localhost:5001/Test/FilterTest2 , se ejecuta el código siguiente:


TestController.OnActionExecuting
MySampleActionFilter.OnActionExecuting
SampleActionFilterAttribute.OnActionExecuting
TestController.FilterTest2
SampleActionFilterAttribute.OnActionExecuted
MySampleActionFilter.OnActionExecuted
TestController.OnActionExecuted

Para Razor Pages, consulte Implementar filtros de páginas de Razor globalmente.


Invalidación del orden predeterminado
La secuencia de ejecución predeterminada se puede invalidar con la implementación de IOrderedFilter.
Order expone la propiedad IOrderedFilter que tiene prioridad sobre el ámbito a la hora de determinar el
orden de ejecución. Un filtro con un valor Order menor:
Ejecuta el código anterior antes que el de un filtro con un valor mayor de Order .
Ejecuta el código posterior después que el de un filtro con un valor mayor de Order .

La propiedad Order se puede establecer con un parámetro de constructor:

[MyFilter(Name = "Controller Level Attribute", Order=1)]

Considere los mismos tres filtros de acción que se muestran en el ejemplo anterior. Si la propiedad Order
del controlador y de los filtros globales está establecida en 1 y 2 respectivamente, el orden de ejecución se
invierte.

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 invalida 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 determina 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 mediante el establecimiento de la
propiedad Result en el parámetro ResourceExecutingContext 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:
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ContentResult()
{
Content = "Resource unavailable - header not 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", "Joe Smith")]


public class SampleController : Controller
{
public IActionResult Index()
{
return Content("Examine the headers using the F12 developer tools.");
}

[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header is set.");
}

Inserción de dependencias
Los filtros se pueden agregar por tipo o por instancia. Si se agrega una instancia, esta se utiliza para todas las
solicitudes. Si se agrega un tipo, se activa por tipo. Un filtro activado por tipo significa:
Se crea una instancia para cada solicitud.
Las dependencias de constructor se rellenan mediante la inserción de dependencias.
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. Las dependencias de constructor no pueden proporcionarse mediante la inserción de
dependencias porque:
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.
Los filtros siguientes admiten dependencias de constructor proporcionadas en la inserción de dependencias:
ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory se implementa en el atributo.
Los filtros anteriores se pueden aplicar a un método de controlador o de acción:
Los registradores están disponibles en la inserción de dependencias. Sin embargo, evite crear y utilizar filtros
únicamente con fines de registro. El registro del marco integrado proporciona normalmente lo que se
necesita para el registro. Registro agregado a los filtros:
Debe centrarse en cuestiones de dominio empresarial o en el comportamiento específico del filtro.
No debe registrar acciones u otros eventos del marco. Los filtros integrados registran acciones y eventos
del marco.
ServiceFilterAttribute
Los tipos de implementación de filtro de servicio se registran en ConfigureServices . ServiceFilterAttribute
recupera una instancia del filtro de la inserción de dependencias.
El código siguiente muestra AddHeaderResultServiceFilter :

public class AddHeaderResultServiceFilter : IResultFilter


{
private ILogger _logger;
public AddHeaderResultServiceFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<AddHeaderResultServiceFilter>();
}

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 started.
}
}

En el código siguiente, AddHeaderResultServiceFilter se agrega al contenedor de inserción de dependencias:

public void ConfigureServices(IServiceCollection services)


{
// Add service filters.
services.AddScoped<AddHeaderResultServiceFilter>();
services.AddScoped<SampleActionFilterAttribute>();

services.AddMvc(options =>
{
options.Filters.Add(new AddHeaderAttribute("GlobalAddHeader",
"Result filter added to MvcOptions.Filters")); // An instance
options.Filters.Add(typeof(MySampleActionFilter)); // By type
options.Filters.Add(new SampleGlobalActionFilter()); // An instance
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

En el código siguiente, el atributo ServiceFilter recupera una instancia del filtro


AddHeaderResultServiceFilter desde la inserción de dependencias:
[ServiceFilter(typeof(AddHeaderResultServiceFilter))]
public IActionResult Index()
{
return View();
}

ServiceFilterAttribute.IsReusable:
Proporciona una sugerencia que la instancia de filtro podría reutilizarse fuera del ámbito de la
solicitud en la que se creó. El entorno de ejecución de ASP.NET Core no garantiza:
Que se creará una única instancia del filtro.
El filtro no volverá a solicitarse desde el contenedor de inserción de dependencias en algún
momento posterior.
No debe usarse con un filtro que depende de servicios con una duración distinta de singleton.
ServiceFilterAttribute implementa IFilterFactory. IFilterFactory expone el método CreateInstance para
crear una instancia de IFilterMetadata. CreateInstance carga el tipo especificado desde la 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.
Dado que los tipos TypeFilterAttribute no se resuelven directamente desde el contenedor de inserción de
dependencias:
Los tipos a los que se hace referencia con TypeFilterAttribute no tienen que estar registrados con el
contenedor de inserción de dependencias. Sus dependencias se completan a través del contenedor de
inserción de dependencias.
TypeFilterAttribute puede aceptar opcionalmente argumentos de constructor del tipo en cuestión.

Al usar TypeFilterAttribute , el valor IsReusable es una sugerencia de que la instancia de filtro podría
reutilizarse fuera del ámbito de la solicitud en la que se creó. El entorno de ejecución de ASP.NET Core no
ofrece ninguna garantía de que se vaya a crear una única instancia del filtro. IsReusable no debe usarse con
un filtro que dependa de servicios con una duración distinta de singleton.
En el siguiente ejemplo se muestra cómo pasar argumentos a un tipo mediante TypeFilterAttribute :

[TypeFilter(typeof(LogConstantFilter),
Arguments = new object[] { "Method 'Hi' called" })]
public IActionResult Hi(string name)
{
return Content($"Hi {name}");
}
public class LogConstantFilter : IActionFilter
{
private readonly string _value;
private readonly ILogger<LogConstantFilter> _logger;

public LogConstantFilter(string value, ILogger<LogConstantFilter> logger)


{
_logger = logger;
_value = value;
}

public void OnActionExecuting(ActionExecutingContext context)


{
_logger.LogInformation(_value);
}

public void OnActionExecuted(ActionExecutedContext context)


{ }
}

Filtros de autorización
Filtros de autorización:
Son los primeros filtros que se ejecutan en la canalización del filtro.
Controlan el acceso a los métodos de acción.
Tienen un método anterior, pero no uno posterior.
Los filtros de autorización personalizados requieren un marco de autorización personalizado. Es preferible
configurar directivas de autorización o escribir una directiva de autorización personalizada a escribir un filtro
personalizado. El filtro de autorización integrado:
Llama a la autorización del sistema.
No autoriza las solicitudes.
No inicie excepciones dentro de los filtros de autorización:
La excepción no se controlará.
Los filtros de excepciones no controlarán la excepción.
Considere la posibilidad de emitir un desafío cuando se produzca una excepción en un filtro de
autorizaciones.
Aquí encontrará más información sobre la autorización.

Filtros de recursos
Filtros de recursos:
Implementan la interfaz IResourceFilter o IAsyncResourceFilter.
La ejecución encapsula la mayor parte de la canalización de filtro.
Los filtros de autorizaciones son los únicos que se ejecutan antes que los filtros de recursos.
Los filtros de recursos son útiles para cortocircuitar la mayor parte de la canalización. Por ejemplo, un filtro
de almacenamiento en caché puede evitar que se ejecute el resto de la canalización en un acierto de caché.
Ejemplos de filtros de recursos:
El filtro de recursos de cortocircuito mostrado anteriormente.
DisableFormValueModelBindingAttribute:
Evita que el enlace de modelos tenga acceso a los datos del formulario.
Se utiliza cuando hay cargas de archivos muy voluminosos para impedir que los datos del
formulario se lean en la memoria.

Filtros de acciones
IMPORTANT
Los filtros de acción no se aplican a Razor Pages. Razor Pages admite IPageFilter y IAsyncPageFilter. Para más
información, vea Filter methods for Razor Pages (Métodos de filtrado para páginas de Razor).

Filtros de acciones:
Implementan la interfaz IActionFilter o IAsyncActionFilter.
Su ejecución rodea la ejecución de los métodos de acción.
El código siguiente muestra un ejemplo de filtro de acciones:

public class MySampleActionFilter : IActionFilter


{
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
}

public void OnActionExecuted(ActionExecutedContext context)


{
// Do something after the action executes.
}
}

ActionExecutingContext ofrece las siguientes propiedades:


ActionArguments: permite leer las entradas de un método de acción.
Controller: permite manipular la instancia del controlador.
Result: si se establece Result , se cortocircuita la ejecución del método de acción y de los filtros de
acciones posteriores.
Inicio de una excepción en un método de acción:
Impide la ejecución de los filtros subsiguientes.
A diferencia del establecimiento de Result , se trata como un error en lugar de como un resultado
correcto.
ActionExecutedContext proporciona Controller y Result , además de las siguientes propiedades:
Canceled: es true si otro filtro ha cortocircuitado la ejecución de la acción.
Exception: es un valor distinto de NULL si la acción o un filtro de acción de ejecución anterior han
producido una excepción. Si se establece esta propiedad en un valor NULL:
Controla la excepción eficazmente.
Result se ejecuta como si se devolviera desde el método de acción.

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 Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.Result a una
instancia de resultado y no llame a next (la clase ActionExecutionDelegate ).
El marco proporciona una clase ActionFilterAttribute abstracta de la que se pueden crear subclases.
Se puede usar el filtro de acción OnActionExecuting para:
Validar el estado del modelo.
Devolver un error si el estado no es válido.

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 Result.
Canceled se establece en true si otro filtro ha cortocircuitado la ejecución de la acción.
Exception se establece en un valor distinto de NULL si la acción o un filtro de acción posterior han
producido una excepción. Si Exception se establece como nulo:
Controla una excepción eficazmente.
ActionExecutedContext.Result se ejecuta como si se devolviera con normalidad desde el método de
acción.
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}

public override void OnActionExecuted(ActionExecutedContext context)


{
var result = context.Result;
// Do something with Result.
if (context.Canceled == true)
{
// Action execution was short-circuited by another filter.
}

if(context.Exception != null)
{
// Exception thrown by action or action filter.
// Set to null to handle the exception.
context.Exception = null;
}
base.OnActionExecuted(context);
}
}

Filtros de excepciones
Los filtros de excepciones:
Implementan IExceptionFilter o IAsyncExceptionFilter.
Se pueden usar para implementar directivas de control de errores comunes.
En el siguiente filtro de excepciones de ejemplo se usa una vista de error 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())
{
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 en Razor Pages, 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 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.
Los filtros de excepciones:
Son adecuados para interceptar las excepciones que se producen en las acciones.
No son tan flexibles como el middleware de control de errores.
Es preferible usar middleware de control de excepciones. Utilice los filtros de excepciones solo cuando el
control de errores es diferente en función del método de acción que se llama. Por ejemplo, puede que una
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.

Filtros de resultados
Filtros de resultados:
Implementar una interfaz:
IResultFilter o IAsyncResultFilter
IAlwaysRunResultFilter o IAsyncAlwaysRunResultFilter
Su ejecución rodea la ejecución de los resultados de acción.
IResultFilter e IAsyncResultFilter
El código siguiente muestra un filtro de resultados que agrega un encabezado HTTP:

public class AddHeaderResultServiceFilter : IResultFilter


{
private ILogger _logger;
public AddHeaderResultServiceFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<AddHeaderResultServiceFilter>();
}

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 started.
}
}

El tipo de resultado que se ejecute dependerá de la acción. Una acción 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 Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuting puede cortocircuitar la
ejecución del resultado de la acción y de los filtros de resultados posteriores mediante el establecimiento de
Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext.Cancel en true . Escriba en el objeto de respuesta
cuando el proceso se cortocircuite, ya que así evitará que se genere una respuesta vacía. Si se inicia una
excepción en IResultFilter.OnResultExecuting , 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 se ejecuta el método Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuted:
Es probable que la respuesta se haya enviado al cliente y no se pueda cambiar.
Si se inicia una excepción, no se envía el cuerpo de respuesta.
ResultExecutedContext.Canceled se establece en true si otro filtro ha cortocircuitado la ejecución del
resultado de la acción.
ResultExecutedContext.Exception se establece 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 ASP.NET Core vuelva a producir dicha excepción más
adelante en la canalización. No hay una forma confiable de escribir datos en una respuesta cuando se
controla una excepción en un filtro de resultados. Si los encabezados ya se han vaciado en el cliente si el
resultado de una acción inicia una excepción, 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 cortocircuitarlo, establezca
ResultExecutingContext.Cancel en true y no llame a ResultExecutionDelegate :

public class MyAsyncResponseFilter : IAsyncResultFilter


{
public async Task OnResultExecutionAsync(ResultExecutingContext context,
ResultExecutionDelegate next)
{
if (!(context.Result is EmptyResult))
{
await next();
}
else
{
context.Cancel = true;
}

}
}

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.
IAlwaysRunResultFilter e IAsyncAlwaysRunResultFilter
Las interfaces IAlwaysRunResultFilter e IAsyncAlwaysRunResultFilter declaran una implementación
IResultFilter que se ejecuta para obtener todos los resultados de la acción. El filtro se aplica a todos los
resultados de la acción a menos que:
Se aplique un IExceptionFilter o IAuthorizationFilter y provoque un cortocircuito en la respuesta.
Un filtro de excepciones controla una excepción al producir un resultado de acción.
Los filtros distintos de IExceptionFilter e IAuthorizationFilter no cortocircuitan IAlwaysRunResultFilter y
IAsyncAlwaysRunResultFilter .
Por ejemplo, el siguiente filtro siempre ejecuta y establece un resultado de la acción (ObjectResult) con un
código de estado 422 - Entidad no procesable cuando se produce un error en la negociación de contenido:

public class UnprocessableResultFilter : Attribute, IAlwaysRunResultFilter


{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is StatusCodeResult statusCodeResult &&
statusCodeResult.StatusCode == 415)
{
context.Result = new ObjectResult("Can't process this!")
{
StatusCode = 422,
};
}
}

public void OnResultExecuted(ResultExecutedContext context)


{
}
}

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 entorno de
ejecución 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 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 con las implementaciones de atributos personalizados 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[] { "My header" });
}

public void OnResultExecuted(ResultExecutedContext context)


{
}
}

public bool IsReusable


{
get
{
return false;
}
}
}

El código anterior se puede probar mediante la ejecución del ejemplo de descargar:


Invoque las herramientas de desarrollador de F12.
Navegue a https://localhost:5001/Sample/HeaderWithFactory .

Las herramientas de desarrollador F12 muestran los siguientes encabezados de respuesta agregados por el
código de ejemplo:
author: Joe Smith
globaladdheader: Result filter added to MvcOptions.Filters
internal: My header
El código anterior crea el encabezado de respuesta internal: My header .
IFilterFactory implementado en un atributo
Los filtros que implementan IFilterFactory son útiles para los filtros que:
No requieren pasar parámetros.
Tienen dependencias de constructor que deben completarse por medio de la inserción de dependencias.
TypeFilterAttribute implementa IFilterFactory. IFilterFactory expone el método CreateInstance para crear
una instancia de IFilterMetadata. CreateInstance carga el tipo especificado desde el contenedor de servicios
(inserción de dependencias).

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

El código siguiente muestra tres métodos para aplicar [SampleActionFilter] :

[SampleActionFilter]
public IActionResult FilterTest()
{
return Content($"From FilterTest");
}

[TypeFilter(typeof(SampleActionFilterAttribute))]
public IActionResult TypeFilterTest()
{
return Content($"From ServiceFilterTest");
}

// ServiceFilter must be registered in ConfigureServices or


// System.InvalidOperationException: No service for type '<filter>' has been registered.
// Is thrown.
[ServiceFilter(typeof(SampleActionFilterAttribute))]
public IActionResult ServiceFilterTest()
{
return Content($"From ServiceFilterTest");
}

En el código anterior, decorar el método con [SampleActionFilter] es el enfoque preferido para aplicar
SampleActionFilter .

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 del entorno de ejecución de ASP.NET Core, lo que significa que tienen acceso al contexto y las
construcciones de ASP.NET Core.
Para usar middleware como un filtro, cree un tipo con un método Configure en el que se especifique el
middleware para insertar en la canalización de filtro. El ejemplo siguiente usa el 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);
}
}

Use MiddlewareFilterAttribute para ejecutar el middleware:

[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
Consulte Métodos de filtrado para páginas de Razor.
Para experimentar con los filtros, descargue, pruebe y modifique el ejemplo de GitHub.
Áreas de ASP.NET Core
21/05/2019 • 14 minutes to read • Edit Online

Por Dhananjay Kumar y Rick Anderson


Las áreas son una característica 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 , o bien a un elemento page de página de Razor.
Las áreas ofrecen una manera de dividir una aplicación web ASP.NET Core en grupos funcionales más
pequeños, cada uno con su propio conjunto de Razor Pages, controladores, vistas y modelos. Un área es en
realidad una estructura dentro de una aplicación. En un proyecto web ASP.NET Core, los componentes
lógicos como Páginas, Modelo, Vista y Controlador se mantienen en carpetas diferentes. El runtime de
ASP.NET Core usa 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 las de
finalización de la compra, facturación y búsqueda. Cada una de estas unidades tiene un área propia para
controlar vistas, controladores, Razor Pages y modelos.
Considere el uso de áreas en un proyecto en los casos siguientes:
La aplicación está formada por varios componentes funcionales generales que se pueden separar de
forma lógica.
Le interesa dividir la aplicación para que se pueda trabajar en cada área funcional de forma
independiente.
Vea o descargue el código de ejemplo (cómo descargarlo). En el ejemplo de descarga se proporciona una
aplicación básica para probar las áreas.
Si va a usar Razor Pages, consulte Áreas con Razor Pages en este documento.

Áreas para controladores con vistas


Una aplicación web ASP.NET Core típica en la que se usen áreas, controladores y vistas contiene lo
siguiente:
Una estructura de carpetas de área.
Controladores decorados con el atributo [Area] para asociar el controlador con el área:

[Area("Products")]
public class ManageController : Controller
{

La ruta de área agregada al inicio:


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

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

Estructura de carpetas de área


Considere una aplicación que tiene dos grupos lógicos, Productos y Servicios. Mediante las áreas, la
estructura de carpetas sería similar a la siguiente:
Nombre del proyecto
Áreas
Productos
Controladores
HomeController.cs
ManageController.cs
Vistas
Página principal
Index.cshtml
Administrar
Index.cshtml
About.cshtml
Servicios
Controladores
HomeController.cs
Vistas
Página principal
Index.cshtml
Aunque el diseño anterior es típico cuando se usan áreas, solo los archivos de vista tienen que usar esta
estructura de carpetas. La detección de vista busca un archivo de vista de área coincidente en el orden
siguiente:

/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
/Pages/Shared/<Action-Name>.cshtml

La ubicación de las carpetas que no son de vista, como Controllers y Models, no importa. Por ejemplo, las
carpetas Controllers y Models no son necesarias. El contenido de Controllers y Models es código que se
compila en un archivo .dll. El contenido de Views no se compila hasta que se haya realizado una solicitud a
esa vista.
Asociación del controlador a un área
Los controladores de área se designan con el atributo [Area]:
using Microsoft.AspNetCore.Mvc;

namespace MVCareas.Areas.Products.Controllers
{
[Area("Products")]
public class ManageController : Controller
{
public IActionResult Index()
{
return View();
}

public IActionResult About()


{
return View();
}
}
}

Adición de la ruta de área


Las rutas de área normalmente usan el enrutamiento convencional en lugar del enrutamiento mediante
atributos. 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.
Se puede {area:...} usar como un token en las plantillas de ruta, si el espacio de direcciones URL es
uniforme en todas las áreas:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

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

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

En el código anterior, exists aplica la restricción de que la ruta debe coincidir con un área. El uso de
{area:...} es el mecanismo menos complicado para agregar enrutamiento a las áreas.

En el código siguiente se usa MapAreaRoute para crear dos rutas de área con nombre:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.MapAreaRoute(
name: "MyAreaProducts",
areaName:"Products",
template: "Products/{controller=Home}/{action=Index}/{id?}");

routes.MapAreaRoute(
name: "MyAreaServices",
areaName: "Services",
template: "Services/{controller=Home}/{action=Index}/{id?}");

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

Cuando use MapAreaRoute con ASP.NET Core 2.2, vea este problema de GitHub.
Para más información, vea Enrutamiento de áreas.
Generación de vínculos con áreas de MVC
En el código siguiente de la descarga de ejemplo se muestra la generación de vínculos con el área
especificada:
<li>Anchor Tag Helper links</li>
<ul>
<li>
<a asp-area="Products" asp-controller="Home" asp-action="About">
Products/Home/About
</a>
</li>
<li>
<a asp-area="Services" asp-controller="Home" asp-action="About">
Services About
</a>
</li>
<li>
<a asp-area="" asp-controller="Home" asp-action="About">
/Home/About
</a>
</li>
</ul>
<li>Html.ActionLink generated links</li>
<ul>
<li>
@Html.ActionLink("Product/Manage/About", "About", "Manage",
new { area = "Products" })
</li>
</ul>
<li>Url.Action generated links</li>
<ul>
<li>
<a href='@Url.Action("About", "Manage", new { area = "Products" })'>
Products/Manage/About
</a>
</li>
</ul>

Los vínculos generados con el código anterior son válidos en cualquier parte de la aplicación.
En la descarga de ejemplo se incluye una vista parcial que contiene los vínculos anteriores y los mismos
vínculos sin especificar el área. En el archivo de diseño se hace referencia a la vista parcial, por lo que en
todas las páginas de la aplicación se muestran los vínculos generados. Los vínculos generados sin
especificar el área solo son válidos cuando se les hace referencia desde una página en el mismo área y
controlador.
Cuando no se especifica el área o el controlador, el enrutamiento depende de los valores de ambiente. Los
valores de ruta actuales de la solicitud actual se consideran valores de ambiente para la generación de
vínculos. En muchos casos de la aplicación de ejemplo, el uso de valores de ambiente genera vínculos
incorrectos.
Para más información, vea Enrutar a acciones de controlador.
Diseño Compartido para áreas con el archivo _ViewStart.cshtml
Para compartir un diseño común para toda la aplicación, mueva el archivo _ViewStart.cshtml a la carpeta
raíz de la aplicación.
Cambio de la carpeta de área predeterminada donde se almacenan las vistas
En el código siguiente se cambia la carpeta de área predeterminada de "Areas" a "MyAreas" :
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.AreaViewLocationFormats.Clear();
options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/{1}/{0}.cshtml");
options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});

services.AddMvc();
}

Áreas con Razor Pages


Las áreas con Razor Pages requieren la carpeta Areas/<nombre de área>/Pages en la raíz de la aplicación.
La siguiente estructura de carpetas se usa con la descarga de ejemplo.
Nombre del proyecto
Áreas
Productos
Páginas
_ViewImports
Acerca de
Índice
Servicios
Páginas
Administrar
Acerca de
Índice
Generación de vínculos con Razor Pages y áreas
El código siguiente de la descarga de ejemplo muestra la generación de vínculos con el área especificada
(por ejemplo, asp-area="Products" ):
<li>Anchor Tag Helper links</li>
<ul>
<li>
<a asp-area="Products" asp-page="/About">
Products/About
</a>
</li>
<li>
<a asp-area="Services" asp-page="/Manage/About">
Services/Manage/About
</a>
</li>
<li>
<a asp-area="" asp-page="/About">
/About
</a>
</li>
</ul>
<li>Url.Page generated links</li>
<ul>
<li>
<a href='@Url.Page("/Manage/About", new { area = "Services" })'>
Services/Manage/About
</a>
</li>
<li>
<a href='@Url.Page("/About", new { area = "Products" })'>
Products/About
</a>
</li>
</ul>

Los vínculos generados con el código anterior son válidos en cualquier parte de la aplicación.
En la descarga de ejemplo se incluye una vista parcial que contiene los vínculos anteriores y los mismos
vínculos sin especificar el área. En el archivo de diseño se hace referencia a la vista parcial, por lo que en
todas las páginas de la aplicación se muestran los vínculos generados. Los vínculos generados sin
especificar el área solo son válidos cuando se les hace referencia desde una página de la misma área.
Cuando no se especifica el área, el enrutamiento depende de los valores ambient. Los valores de ruta
actuales de la solicitud actual se consideran valores de ambiente para la generación de vínculos. En muchos
casos de la aplicación de ejemplo, el uso de valores de ambiente genera vínculos incorrectos. Por ejemplo,
tenga en cuenta los vínculos generados desde el código siguiente:

<li>
<a asp-page="/Manage/About">
Services/Manage/About
</a>
</li>
<li>
<a asp-page="/About">
/About
</a>
</li>

En el código anterior:
El vínculo generado a partir de <a asp-page="/Manage/About"> es correcto solo cuando la última solicitud
fue de una página del área Services . Por ejemplo, /Services/Manage/ , /Services/Manage/Index o
/Services/Manage/About .
El vínculo generado a partir de <a asp-page="/About"> es correcto solo cuando la última solicitud fue de
una página de /Home .
El código está tomado de la descarga de ejemplo.
Importación del espacio de nombres y los asistentes de etiquetas con el archivo _ViewImports
Se puede agregar un archivo _ViewImports.cshtml a la carpeta Pages de cada área para importar el espacio
de nombres y los asistentes de etiquetas en cada página de Razor de la carpeta.
Tenga en cuenta el área Services del código de ejemplo, que no contiene un archivo _ViewImports.cshtml. El
marcado siguiente muestra la página de Razor /Services/Manage/About:

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RPareas.Areas.Services.Pages.Manage.AboutModel
@{
ViewData["Title"] = "Srv Mng About";
}

<h2>/Services/Manage/About</h2>

<a asp-area="Products" asp-page="/Index">


Products/Index
</a>

En el marcado anterior:
El nombre de dominio completo debe usarse para especificar el modelo (
@model RPareas.Areas.Services.Pages.Manage.AboutModel ).
Los asistentes de etiquetas se habilitan mediante @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

En la descarga de ejemplo, el área Products contiene el siguiente archivo _ViewImports.cshtml:

@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El siguiente marcado muestra la página de Razor /Products/About:

@page
@model AboutModel
@{
ViewData["Title"] = "Prod About";
}

<h2>Products/About</h2>

<a asp-area="Services" asp-page="/Manage/About">


Services/Manage/About
</a>

En el archivo anterior, el espacio de nombres y la directiva @addTagHelper se importan en el archivo


mediante el archivo Areas/Products/Pages/_ViewImports.cshtml.
Para más información, consulte Administración del ámbito de los asistentes de etiquetas y Importar
directivas compartidas.
Diseño compartido para áreas de Razor Pages
Para compartir un diseño común para toda la aplicación, mueva el archivo _ViewStart.cshtml a la carpeta
raíz de la aplicación.
Publicación de áreas
Todos los archivos *.cshtml y los archivos que formen parte del directorio wwwroot se publicarán como
resultados si <Project Sdk="Microsoft.NET.Sdk.Web"> está incluido en el archivo *.csproj.
Elementos de aplicación en ASP.NET Core
10/05/2019 • 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: muestra de 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:


Creación de API web con ASP.NET Core
17/05/2019 • 15 minutes to read • Edit Online

Por Scott Addie y Tom Dykstra


ASP.NET Core admite la creación de servicios RESTful, lo que también se conoce como API web, mediante C#.
Para gestionar las solicitudes, una API web usa controladores. Los controladores de una API web son clases que
se derivan de ControllerBase . En este artículo se muestra cómo usar controladores para gestionar las
solicitudes de API.
Vea o descargue el código de ejemplo. (Método de descarga).

Clase ControllerBase
Una API web tiene una o varias clases de controlador que se derivan de ControllerBase. Por ejemplo, la plantilla
de proyecto de API web crea un controlador de valores:

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

No cree un controlador de API web mediante la derivación de la clase base Controller. Controller se deriva de
ControllerBase y agrega compatibilidad con vistas, por lo que sirve para gestionar páginas web, no solicitudes
de API web. Hay una excepción a esta regla: si tiene pensado usar el mismo controlador tanto para vistas como
para API, debe derivarlo de Controller .
La clase ControllerBase ofrece muchas propiedades y métodos que son útiles para gestionar solicitudes HTTP.
Por ejemplo, ControllerBase.CreatedAtAction devuelve un código de estado 201:

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
pet.Id = _petsInMemoryStore.Any() ? _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
_petsInMemoryStore.Add(pet);

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

A continuación se muestran algunos ejemplos más de métodos que proporciona ControllerBase .

MÉTODO NOTAS

BadRequest Devuelve el código de estado 400.

NotFound Devuelve el código de estado 404.

PhysicalFile Devuelve un archivo.


MÉTODO NOTAS

TryUpdateModelAsync Invoca el enlace de modelo.

TryValidateModel Invoca la validación de modelos.

Para ver una lista de todos los métodos y propiedades disponibles, consulte ControllerBase.

Atributos
El espacio de nombres Microsoft.AspNetCore.Mvc proporciona atributos que se pueden usar para configurar el
comportamiento de los controladores API web y los métodos de acción. En el ejemplo siguiente se usan
atributos para especificar el método HTTP aceptado y los códigos de estado devueltos:

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
pet.Id = _petsInMemoryStore.Any() ? _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
_petsInMemoryStore.Add(pet);

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

Estos son algunos ejemplos más de atributos que están disponibles.

ATRIBUTO NOTAS

[Route] Especifica el patrón de dirección URL de un controlador o


una acción.

[Bind] Especifica el prefijo y las propiedades que se incluirán en el


enlace de modelo.

[HttpGet] Identifica una acción que admite el método HTTP GET.

[Consumes] Especifica los tipos de datos que acepta una acción.

[Produces] Especifica los tipos de datos que devuelve una acción.

Para ver una lista que incluye los atributos disponibles, consulte el espacio de nombres
Microsoft.AspNetCore.Mvc.

Atributo ApiController
El atributo [ApiController] puede aplicarse a una clase de controlador para permitir comportamientos
específicos de API:
Requisito de enrutamiento mediante atributos
Respuestas HTTP 400 automáticas
Inferencia de parámetro de origen de enlace
Inferencia de solicitud de varios elementos o datos de formulario
Detalles de problemas de los códigos de estado de error
Estas características requieren una versión de compatibilidad de 2.1 o posterior.
ApiController en controladores específicos
El atributo [ApiController] puede aplicarse a controladores específicos, como se muestra en el siguiente
ejemplo de la plantilla de proyecto:

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

ApiController en varios controladores


Una estrategia para el uso del atributo en más de un controlador consiste en crear una clase personalizada de
controlador base anotada con el atributo [ApiController] . Este es un ejemplo que muestra una clase base
personalizada y un controlador que se deriva de ella:

[ApiController]
public class MyControllerBase : ControllerBase
{
}

[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : MyControllerBase

ApiController en un ensamblado
Si la versión de compatibilidad está establecida en 2.2 o posterior, el atributo [ApiController] se puede aplicar a
un ensamblado. En este contexto, la anotación aplica el comportamiento de API web a todos los controladores
del ensamblado. No hay ninguna manera de excluir controladores específicos. Aplique el atributo de nivel de
ensamblado a la clase Startup , tal y como se muestra en el ejemplo siguiente:

[assembly: ApiController]
namespace WebApiSample
{
public class Startup
{
...
}
}

Requisito de enrutamiento mediante atributos


El atributo ApiController convierte el enrutamiento de atributos en un requisito. Por ejemplo:

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

Las acciones no son accesibles mediante rutas convencionales definidas por UseMvc o
UseMvcWithDefaultRoute en Startup.Configure .

Respuestas HTTP 400 automáticas


El atributo ApiController hace que los errores de validación de un modelo desencadenen automáticamente una
respuesta HTTP 400. Por lo tanto, el siguiente código no es necesario en un método de acción:

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

Respuesta BadRequest predeterminada


Con una versión de compatibilidad de 2.2 o posterior, el tipo de respuesta predeterminado para las respuestas
HTTP 400 es ValidationProblemDetails. El tipo ValidationProblemDetails cumple los requisitos de la
especificación RFC 7807.
Para cambiar la respuesta predeterminada por SerializableError, establezca la propiedad
SuppressUseValidationProblemDetailsForInvalidModelStateResponses como true en Startup.ConfigureServices ,
como se muestra en el ejemplo siguiente:

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

Respuesta BadRequest personalizada


Para personalizar la respuesta que es consecuencia de un error de validación, use
InvalidModelStateResponseFactory. Agregue el código resaltado siguiente después de
services.AddMvc().SetCompatibilityVersion :

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Type = "https://contoso.com/probs/modelvalidation",
Title = "One or more model validation errors occurred.",
Status = StatusCodes.Status400BadRequest,
Detail = "See the errors property for details.",
Instance = context.HttpContext.Request.Path
};

return new BadRequestObjectResult(problemDetails)


{
ContentTypes = { "application/problem+json" }
};
};
});

Registro de respuestas 400 automáticas


Consulte cómo registrar respuestas 400 automáticas sobre errores de validación de modelos
(aspnet/AspNetCore.Docs #12157).
Deshabilitación de respuestas 400 automáticas
Para deshabilitar el comportamiento 400 automático, establezca la propiedad SuppressModelStateInvalidFilter
en true . Agregue el código resaltado siguiente en Startup.ConfigureServices después de
services.AddMvc().SetCompatibilityVersion :

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

Inferencia de parámetro de origen de enlace


Un atributo de origen de enlace define la ubicación del valor del parámetro de una acción. Existen los atributos
de origen de enlace siguientes:

ATRIBUTO ORIGEN DE ENLACE

[FromBody] Cuerpo de la solicitud

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

[FromHeader] Encabezado de la solicitud

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

[FromRoute] Datos de ruta de la solicitud actual

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

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 atributo [ApiController] o los atributos de origen de enlace, como [FromQuery] , el entorno de tiempo de
ejecución de ASP.NET Core intenta usar el enlazador de modelos de objetos complejos. El enlazador de
modelos de objetos complejos extrae los datos de los proveedores de valor en un orden definido.
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 ActionResult<List<Product>> Get(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;

if (discontinuedOnly)
{
products = _productsInMemoryStore.Where(p => p.IsDiscontinued).ToList();
}
else
{
products = _productsInMemoryStore;
}

return products;
}

El atributo [ApiController] aplica reglas de inferencia a los orígenes de datos predeterminados de los
parámetros de acción. Estas reglas aplican atributos a los parámetros de acción, lo que ahorra la necesidad de
identificar los orígenes de enlace manualmente. Las reglas de inferencia de orígenes de enlace se comportan de
la manera siguiente:
[FromBody] se infiere para parámetros de tipo complejo. La excepción a [FromBody] 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.
[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.

Notas de la inferencia de FromBody


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 tiene más de un parámetro enlazado desde el cuerpo de solicitud, se produce una excepción.
Por ejemplo, todas las firmas de acción siguientes provocan una excepción:
[FromBody] se infiere en ambos porque son tipos complejos.

[HttpPost]
public IActionResult Action1(Product product, Order order)

El atributo [FromBody] en uno se infiere en el otro porque es un tipo complejo.

[HttpPost]
public IActionResult Action2(Product product, [FromBody] Order order)

El atributo [FromBody] se infiere en ambos.

[HttpPost]
public IActionResult Action3([FromBody] Product product, [FromBody] Order order)
NOTE
En ASP.NET Core 2.1, los parámetros de tipo de colección, como listas y matrices, se infieren incorrectamente como
[FromQuery] . El atributo [FromBody] debe usarse con estos parámetros si van a enlazarse desde el cuerpo de solicitud.
Este comportamiento se ha corregido en ASP.NET Core 2.2 o posterior, donde se infieren los parámetros de tipo de
colección para enlazarse desde el cuerpo de forma predeterminada.

Deshabilitación de las reglas de inferencia


Para deshabilitar la inferencia del origen de enlace, establezca SuppressInferBindingSourcesForParameters en
true . Agregue el código siguiente en Startup.ConfigureServices tras
services.AddMvc().SetCompatibilityVersion :

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

Inferencia de solicitud de varios elementos o datos de formulario


El atributo [ApiController] aplica una regla de inferencia cuando se anota un parámetro de acción con el
atributo [FromForm]: se infiere el tipo de contenido de solicitud multipart/form-data .
Para deshabilitar el comportamiento predeterminado, establezca
SuppressConsumesConstraintForFormFileParameters como true en Startup.ConfigureServices , como se
muestra en el ejemplo siguiente:

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

Detalles de problemas de los códigos de estado de error


Cuando la versión de compatibilidad es 2.2 o posterior, MVC transforma un resultado de error (un resultado con
el código de estado 400 o superior) en un resultado con ProblemDetails. El tipo ProblemDetails se basa en la
especificación RFC 7807 para proporcionar detalles de error de lectura mecánica en una respuesta HTTP.
Observe el código siguiente en una acción de controlador:
if (pet == null)
{
return NotFound();
}

La respuesta HTTP para NotFound tiene un código de estado 404 con un cuerpo ProblemDetails . Por ejemplo:

{
type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
title: "Not Found",
status: 404,
traceId: "0HLHLV31KRN83:00000001"
}

Respuesta ProblemDetails personalizada


Use la propiedad ClientErrorMapping para configurar el contenido de la respuesta ProblemDetails . Por ejemplo,
el código siguiente permite actualizar la propiedad type para respuestas 404:

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

Deshabilitación de la respuesta ProblemDetails


La creación automática de ProblemDetails está deshabilitada cuando la propiedad SuppressMapClientErrors está
establecida en true . Agregue el siguiente código en Startup.ConfigureServices :

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
options.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
options.ClientErrorMapping[404].Link =
"https://httpstatuses.com/404";
});

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
Tutorial: Creación de una API web con ASP.NET
Core
05/07/2019 • 29 minutes to read • Edit Online

Por Rick Anderson y Mike Wasson


En este tutorial se enseñan los conceptos básicos de la compilación de una API web con ASP.NET Core.
En este tutorial aprenderá a:
Crear un proyecto de API web.
Agregar una clase de modelo.
Crear el contexto de la base de datos.
Registrar el contexto de la base de datos.
Agregar un controlador.
Agregar métodos CRUD.
Configurar el enrutamiento y las rutas de dirección URL.
Especificar los valores devueltos.
Llamar a la API web con Postman.
Llamar a la API web con jQuery.
Al final, tendrá una API web que pueda administrar las tareas "pendientes" almacenadas en una base de datos
relacional.

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

API DESCRIPCIÓN 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 None Tarea pendiente


identificador

POST /api/todo Incorporación de un nuevo Tarea pendiente Tarea pendiente


elemento

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


existente

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

En el diagrama siguiente, se muestra el diseño de la aplicación.


Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio 2019 with the ASP.NET and web development workload
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work
with Visual Studio.

Creación de un proyecto web


Visual Studio
Visual Studio Code
Visual Studio para Mac
En el menú Archivo, seleccione Nuevo > Proyecto.
Seleccione la plantilla Aplicación web ASP.NET Core y haga clic en Siguiente.
Asigne al proyecto el nombre TodoApi y haga clic en Crear.
En el cuadro de diálogo Crear una aplicación web ASP.NET Core, confirme que las opciones .NET Core y
ASP.NET Core 2.2 estén seleccionadas. Seleccione la plantilla API y haga clic en Crear. No seleccione
Habilitar compatibilidad con Docker.
Prueba de la API
La plantilla del proyecto crea una API values . Llame al método Get desde un explorador para probar la
aplicación.
Visual Studio
Visual Studio Code
Visual Studio para Mac
Presione Ctrl+F5 para ejecutar la aplicación. Visual Studio inicia un explorador y navega hasta
https://localhost:<port>/api/values , donde <port> es un número de puerto elegido aleatoriamente.

Si aparece un cuadro de diálogo en que se le pregunta si debe confiar en el certificado de IIS Express, seleccione
Sí. En el cuadro de diálogo Advertencia de seguridad que aparece a continuación, seleccione Sí.
Se devuelve el siguiente JSON:

["value1","value2"]

Incorporación de una clase de modelo


Un modelo es un conjunto de clases que representan los datos que la aplicación administra. El modelo para esta
aplicación es una clase TodoItem única.
Visual Studio
Visual Studio Code
Visual Studio para Mac
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.
Haga clic con el botón derecho en la carpeta Models y seleccione Agregar > Clase. Asigne a la clase el
nombre TodoItem y seleccione Agregar.
Reemplace el código de plantilla por el código siguiente:

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

La propiedad Id funciona como clave única en una base de datos relacional.


Las clases de modelo pueden ir en cualquier lugar del proyecto, pero convencionalmente e usa la carpeta
Models.

Incorporación de un 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. Esta clase se crea derivándola de la clase Microsoft.EntityFrameworkCore.DbContext .
Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Models y seleccione Agregar > Clase. Asigne a la clase el
nombre TodoContext y haga clic en Agregar.
Reemplace el código de plantilla por el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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


}
}

Registro del contexto de base de datos


En ASP.NET Core, los servicios (como el contexto de la base de datos) deben registrarse con el contenedor de
inserción de dependencias (DI). El contenedor proporciona el servicio a los controladores.
Actualice Startup.cs con el siguiente código resaltado:
// Unused usings removed
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;

namespace TodoApi
{
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.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

// 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
{
// The default HSTS value is 30 days. You may want to change this for
// production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseMvc();
}
}
}

El código anterior:
Elimina las declaraciones using no utilizadas.
Agrega el contexto de base de datos para el contenedor de DI.
Especifica que el contexto de base de datos usará una base de datos en memoria.

Incorporación de un controlador
Visual Studio
Visual Studio Code/Visual Studio para Mac
Haga clic con el botón derecho en la carpeta Controllers.
Seleccione Agregar > Nuevo elemento.
En el cuadro de diálogo Agregar nuevo elemento, seleccione la plantilla Clase de controlador de
API.
Asigne a la clase el nombre TodoController y seleccione Agregar.

Reemplace el código de plantilla por el código siguiente:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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.
Representa la clase con el atributo [ApiController]. Este atributo indica que el controlador responde a las
solicitudes de la API web. Para información sobre comportamientos específicos que permite el atributo,
consulte Creación de API web con ASP.NET Core.
Utiliza 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.
Si la base de datos está vacía, le agrega un elemento denominado Item1 . Este código está en el constructor,
de manera que se ejecuta cada vez que hay una nueva solicitud HTTP. Si elimina todos los elementos, el
constructor volverá a crear Item1 la próxima vez que se llame a un método de API. De este modo, es posible
que parezca que la eliminación no ha funcionado, cuando en realidad sí lo ha hecho.

Incorporación de métodos Get


Para proporcionar una API que recupere tareas pendientes, agregue estos métodos a la clase TodoController :

// GET: api/Todo
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
return await _context.TodoItems.ToListAsync();
}

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Estos métodos implementan dos puntos de conexión GET:


GET /api/todo
GET /api/todo/{id}

Llame a los dos puntos de conexión desde un explorador para probar la aplicación. Por ejemplo:
https://localhost:<port>/api/todo
https://localhost:<port>/api/todo/1

La llamada a GetTodoItems genera la siguiente respuesta HTTP:

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

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:
Comience por la cadena de plantilla en el atributo Route del controlador:

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

Reemplace [controller] por el nombre del controlador, que convencionalmente es el nombre de clase
de controlador sin el sufijo "Controller". En este ejemplo, el nombre de clase de controlador es
TodoController; por tanto, el nombre del controlador 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 (por ejemplo, [HttpGet("products")] ), anéxela 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 GetTodoItem , "{id}" es una variable de marcador de posición correspondiente al
identificador único de la tarea pendiente. Al invocar a GetTodoItem , el valor "{id}" de la dirección URL se
proporciona al método en su parámetro id .

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Valores devueltos
El tipo de valor devuelto de los métodos GetTodoItems y GetTodoItem es ActionResult<T > type. ASP.NET Core
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 tipo de valor devuelto es el 200, suponiendo que no haya ninguna excepción no
controlada. Las excepciones no controladas se convierten en errores 5xx.
Los tipos de valores devueltos ActionResult pueden representar una gama amplia de códigos de estado HTTP.
Por ejemplo, GetTodoItem puede devolver dos valores de estado diferentes:
Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de
error 404 NotFound.
En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una
respuesta HTTP 200.

Prueba del método GetTodoItems


En este tutorial se usa Postman para probar la API web.
Instale Postman.
Inicie la aplicación web.
Inicie Postman.
Deshabilite Comprobación del certificado SSL.
En Archivo > Configuración (pestaña *General), deshabilite Comprobación del certificado SSL.

WARNING
Vuelva a habilitar la comprobación del certificado SSL tras probar el controlador.

Cree una nueva solicitud.


Establezca el método HTTP en GET.
Establezca la dirección URL de la solicitud en https://localhost:<port>/api/todo . Por ejemplo:
https://localhost:5001/api/todo .
Establezca Vista de dos paneles en Postman.
Seleccione Enviar.

Incorporación de un método Create


Agregue el siguiente método PostTodoItem :
// POST: api/Todo
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem item)
{
_context.TodoItems.Add(item);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item);


}

El código anterior es un método HTTP POST, según indica el atributo [HttpPost]. El método obtiene el valor de
tareas pendientes del cuerpo de la solicitud HTTP.
El método CreatedAtAction realiza las acciones siguientes:
Devuelve un código de estado HTTP 201 cuando se ha ejecutado correctamente. HTTP 201 es la
respuesta estándar para un método HTTP POST que crea un recurso en el servidor.
Agrega un encabezado Location a la respuesta. El encabezado Location especifica el identificador URI
de la tarea pendiente recién creada. Para obtener más información, consulte 10.2.2 201 creado.
Hace referencia a la acción GetTodoItem para crear el identificador URI del encabezado Location . La
palabra clave nameof de C# se usa para evitar que se codifique de forma rígida el nombre de acción en la
llamada a CreatedAtAction .

// GET: api/Todo/5
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

return todoItem;
}

Prueba del método PostTodoItem


Compile el proyecto.
En Postman, establezca el método HTTP en POST .
Seleccione la pestaña Cuerpo.
Seleccione el botón de radio Raw (Sin formato).
Establezca el tipo en JSON (application/json) .
En el cuerpo de la solicitud, introduzca JSON para una tarea pendiente:

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

Seleccione Enviar.
Si recibe un error 405 (Método no permitido), probablemente sea el resultado de no haber compilado el
proyecto después de agregar el método PostTodoItem .
Prueba del URI del encabezado de ubicación
Seleccione la pestaña Encabezados en el panel Respuesta.
Copie el valor de encabezado Ubicación:

Establezca el método en GET.


Pegue el URI (por ejemplo, https://localhost:5001/api/Todo/2 ).
Seleccione Enviar.

Incorporación de un método PutTodoItem


Agregue el siguiente método PutTodoItem :
// PUT: api/Todo/5
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem item)
{
if (id != item.Id)
{
return BadRequest();
}

_context.Entry(item).State = EntityState.Modified;
await _context.SaveChangesAsync();

return NoContent();
}

PutTodoItem es similar a PostTodoItem , 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 cambios. Para admitir actualizaciones parciales, use HTTP PATCH.
Si recibe un error al llamar a PutTodoItem , llame a GET para asegurarse de que hay un elemento en la base de
datos.
Prueba del método PutTodoItem
En este ejemplo se usa una base de datos en memoria que se debe iniciar cada vez que se inicia la aplicación.
Debe haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para
asegurarse de que hay un elemento en la base de datos antes de realizar una llamada PUT.
Actualice la tarea pendiente que tiene el id. = 1 y establezca su nombre en "feed fish":

{
"ID":1,
"name":"feed fish",
"isComplete":true
}

En la imagen siguiente, se muestra la actualización de Postman:


Incorporación de un método DeleteTodoItem
Agregue el siguiente método DeleteTodoItem :

// DELETE: api/Todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

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

_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();

return NoContent();
}

La respuesta de DeleteTodoItem es 204 (Sin contenido).


Prueba del método DeleteTodoItem
Use Postman para eliminar una tarea pendiente:
Establezca el método en DELETE .
Establezca el URI del objeto que quiera eliminar, por ejemplo, https://localhost:5001/api/todo/1 .
Seleccione Enviar.
La aplicación de ejemplo permite eliminar todos los elementos. Sin embargo, al eliminar el último elemento, se
creará uno nuevo en el constructor de clase de modelo la próxima vez que se llame a la API.

Llamada a la API con jQuery


En esta sección, se agrega una página HTML que usa jQuery para llamar a la API web. jQuery inicia la solicitud
y actualiza la página con los detalles de la respuesta de la API.
Configure la aplicación para atender archivos estáticos y habilitar la asignación de archivos predeterminada
mediante la actualización de Startup.cs con el siguiente código resaltado:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for
// production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
}
Cree una carpeta wwwroot en el directorio del proyecto.
Agregue un archivo HTML denominado index.html al directorio wwwroot. 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="Save">
<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"
<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. 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.text("No " + name);
}
}

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

function getData() {
$.ajax({
type: "GET",
url: uri,
cache: false,
success: function(data) {
const tBody = $("#todos");

$(tBody).empty();

getCount(data.length);

$.each(data, function(key, item) {


const tr = $("<tr></tr>")
.append(
$("<td></td>").append(
$("<input/>", {
type: "checkbox",
disabled: true,
checked: item.isComplete
})
)
)
.append($("<td></td>").text(item.name))
.append(
$("<td></td>").append(
$("<button>Edit</button>").on("click", function() {
editItem(item.id);
})
)
)
.append(
$("<td></td>").append(
$("<button>Delete</button>").on("click", function() {
deleteItem(item.id);
})
)
);
tr.appendTo(tBody);
});

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("Something went wrong!");
},
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
probar la página HTML localmente:
Abra Properties\launchSettings.json.
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.
En este ejemplo se llama a todos los métodos CRUD de la API. A continuación, encontrará algunas
explicaciones de las llamadas a la API.
Obtención de una lista de tareas pendientes
La función de JQuery ajax envía una solicitud GET a la API, que devuelve código JSON que representa una
matriz de tareas pendientes. 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,
cache: false,
success: function(data) {
const tBody = $("#todos");

$(tBody).empty();

getCount(data.length);

$.each(data, function(key, item) {


const tr = $("<tr></tr>")
.append(
$("<td></td>").append(
$("<input/>", {
type: "checkbox",
disabled: true,
checked: item.isComplete
})
)
)
.append($("<td></td>").text(item.name))
.append(
$("<td></td>").append(
$("<button>Edit</button>").on("click", function() {
editItem(item.id);
})
)
)
.append(
$("<td></td>").append(
$("<button>Delete</button>").on("click", function() {
deleteItem(item.id);
})
)
);

tr.appendTo(tBody);
});

todos = data;
}
});
}

Incorporación de una tarea pendiente


La función ajax envía una solicitud POST con la tarea pendiente en su cuerpo. Las opciones accepts y
contentType se establecen en application/json para especificar el tipo de medio que se va a recibir y a enviar.
La tarea pendiente se convierte en JSON mediante 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("Something went wrong!");
},
success: function(result) {
getData();
$("#add-name").val("");
}
});
}

Actualizar una tarea pendiente


El hecho de actualizar una tarea pendiente es similar al de agregar una. El valor url cambia para agregar 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 el valor 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();
}
});

Recursos adicionales
Vea o descargue el código de ejemplo para este tutorial. Vea cómo descargarlo.
Para obtener más información, vea los siguientes recursos:
Creación de API web con ASP.NET Core
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Páginas de Razor de ASP.NET Core con EF Core: serie de tutoriales
Enrutar a acciones de controlador de ASP.NET Core
Tipos de valor devuelto de acción del controlador de ASP.NET Core Web API
Implementar aplicaciones de ASP.NET Core en Azure App Service
Hospedaje e implementación de ASP.NET Core
Versión en YouTube de este tutorial

Pasos siguientes
En este tutorial ha aprendido a:
Crear un proyecto de API web.
Agregar una clase de modelo.
Crear el contexto de la base de datos.
Registrar el contexto de la base de datos.
Agregar un controlador.
Agregar métodos CRUD.
Configurar el enrutamiento y las rutas de dirección URL.
Especificar los valores devueltos.
Llamar a la API web con Postman.
Llamar a la API web con jQuery.
Pase al siguiente tutorial para obtener información sobre cómo generar páginas de ayuda de API:
Introducción a Swashbuckle y ASP.NET Core
Creación de una API Web con ASP.NET Core y
MongoDB
03/07/2019 • 18 minutes to read • Edit Online

Por Pratik Khandelwal y Scott Addie


En este tutorial se crea una API web que realiza operaciones de creación, lectura, actualización y eliminación
(CRUD ) en una base de datos NoSQL de MongoDB.
En este tutorial aprenderá a:
Configurar MongoDB
Crear una base de datos de MongoDB
Definir un esquema y una colección de MongoDB
Realizar operaciones de CRUD de MongoDB desde una API web
Personalizar la serialización de JSON
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
.NET Core SDK 2.2 o posterior
Visual Studio 2019 con la carga de trabajo ASP.NET y desarrollo web
MongoDB

Configurar MongoDB
Si usa Windows, MongoDB está instalado en C:\Archivos de programa\MongoDB de forma predeterminada.
Agregue C:\Archivos de programa\MongoDB\Server\<número_versión>\bin a la variable de entorno Path . Este
cambio permite el acceso a MongoDB desde cualquier lugar en el equipo de desarrollo.
Use el Shell de mongo en los pasos siguientes para crear una base de datos, hacer colecciones y almacenar
documentos. Para obtener más información sobre los comandos de Shell de mongo, consulte Working with the
mongo Shell (Trabajo con el shell de Mongo).
1. Elija un directorio en el equipo de desarrollo para almacenar los datos. Por ejemplo, C:\BooksData en
Windows. Si no existe el directorio, créelo. El shell de mongo no crea nuevos directorios.
2. Abra un shell de comandos. Ejecute el comando siguiente para conectarse a MongoDB en el puerto
predeterminado 27017. No olvide reemplazar <data_directory_path> por el directorio que eligió en el paso
anterior.

mongod --dbpath <data_directory_path>

3. Abra otra instancia del shell de comandos. Conéctese a la base de datos de prueba de forma
predeterminada ejecutando el comando siguiente:
mongo

4. Ejecute lo siguiente en un shell de comandos:

use BookstoreDb

Si aún no existe, se crea una base de datos denominada BookstoreDb. Si la base de datos existe, su conexión
se abre para las transacciones.
5. Cree una colección Books con el comando siguiente:

db.createCollection('Books')

Se muestra el siguiente resultado:

{ "ok" : 1 }

6. Defina un esquema para la colección Books e inserte dos documentos con el comando siguiente:

db.Books.insertMany([{'Name':'Design Patterns','Price':54.93,'Category':'Computers','Author':'Ralph
Johnson'}, {'Name':'Clean Code','Price':43.15,'Category':'Computers','Author':'Robert C. Martin'}])

Se muestra el siguiente resultado:

{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5bfd996f7b8e48dc15ff215d"),
ObjectId("5bfd996f7b8e48dc15ff215e")
]
}

7. Vea los documentos en la base de datos mediante el comando siguiente:

db.Books.find({}).pretty()

Se muestra el siguiente resultado:

{
"_id" : ObjectId("5bfd996f7b8e48dc15ff215d"),
"Name" : "Design Patterns",
"Price" : 54.93,
"Category" : "Computers",
"Author" : "Ralph Johnson"
}
{
"_id" : ObjectId("5bfd996f7b8e48dc15ff215e"),
"Name" : "Clean Code",
"Price" : 43.15,
"Category" : "Computers",
"Author" : "Robert C. Martin"
}
El esquema agrega una propiedad _id generada automáticamente del tipo ObjectId para cada
documento.
La base de datos está lista. Puede empezar a crear la API web de ASP.NET Core.

Creación de un proyecto de API web de ASP.NET Core


Visual Studio
Visual Studio Code
Visual Studio para Mac
1. Vaya a Archivo > Nuevo > Proyecto.
2. Seleccione el tipo de proyecto Aplicación web de ASP.NET Core y, luego, Siguiente.
3. Denomine el proyecto BooksApi y seleccione Crear.
4. Seleccione el marco de destino .NET Core y ASP.NET Core 2.2. Seleccione la plantilla de proyecto API y,
luego, Crear.
5. Visite la galería de NuGet: MongoDB.Driver para determinar la última versión estable del controlador .NET
para MongoDB. En la ventana Consola del Administrador de paquetes, desplácese hasta la raíz del
proyecto. Ejecute el siguiente comando para instalar el controlador .NET para MongoDB:

Install-Package MongoDB.Driver -Version {VERSION}

Adición de un modelo de entidad


1. Agregue un directorio Modelos a la raíz del proyecto.
2. Agregue una clase Book al directorio Modelos con el código siguiente:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BooksApi.Models
{
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }

[BsonElement("Name")]
public string BookName { get; set; }

public decimal Price { get; set; }

public string Category { get; set; }

public string Author { get; set; }


}
}

En la clase anterior, se requiere la propiedad Id

para asignar el objeto de Common Language Runtime (CLR ) a la colección de MongoDB.


Se anota con [BsonId] para designar esta propiedad como clave principal del documento.
Se anota con [BsonRepresentation(BsonType.ObjectId)] para permitir que el parámetro pase como tipo
string en lugar de como una estructura ObjectId. Mongo controla la conversión de string a ObjectId
.
La propiedad BookName se anota con el atributo [BsonElement]. El valor Name del atributo representa el
nombre de propiedad en la colección de MongoDB.

Adición de un modelo configuración


1. Agregue los siguientes valores de configuración de base de datos a appsettings.json:

{
"BookstoreDatabaseSettings": {
"BooksCollectionName": "Books",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookstoreDb"
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

2. Agregue un archivo BookstoreDatabaseSettings.cs al directorio Models con el código siguiente:

namespace BooksApi.Models
{
public class BookstoreDatabaseSettings : IBookstoreDatabaseSettings
{
public string BooksCollectionName { get; set; }
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
}

public interface IBookstoreDatabaseSettings


{
string BooksCollectionName { get; set; }
string ConnectionString { get; set; }
string DatabaseName { get; set; }
}
}

La clase anterior BookstoreDatabaseSettings se utiliza para almacenar los valores de propiedad


BookstoreDatabaseSettings del archivo appsettings.json. Los nombres de las propiedades de JSON y C# son
iguales para facilitar el proceso de asignación.
3. Agregue el código resaltado siguiente a Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

En el código anterior:
La instancia de configuración a la que la sección BookstoreDatabaseSettings del archivo appsettings.json
enlaza está registrada en el contenedor de inserción de dependencias (DI). Por ejemplo, una propiedad
ConnectionString del objeto BookstoreDatabaseSettings se rellena con la propiedad
BookstoreDatabaseSettings:ConnectionString en appsettings.json.
La interfaz IBookstoreDatabaseSettings se registra en la inserción de dependencias con una duración de
servicio de tipo singleton. Cuando se inserta, la instancia de la interfaz se resuelve en un objeto
BookstoreDatabaseSettings .
4. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver las referencias a
BookstoreDatabaseSettings y IBookstoreDatabaseSettings :

using BooksApi.Models;

Adición de un servicio de operaciones CRUD


1. Agregue un directorio Servicios a la raíz del proyecto.
2. Agregue una clase BookService al directorio Servicios con el código siguiente:
using BooksApi.Models;
using MongoDB.Driver;
using System.Collections.Generic;
using System.Linq;

namespace BooksApi.Services
{
public class BookService
{
private readonly IMongoCollection<Book> _books;

public BookService(IBookstoreDatabaseSettings settings)


{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);

_books = database.GetCollection<Book>(settings.BooksCollectionName);
}

public List<Book> Get() =>


_books.Find(book => true).ToList();

public Book Get(string id) =>


_books.Find<Book>(book => book.Id == id).FirstOrDefault();

public Book Create(Book book)


{
_books.InsertOne(book);
return book;
}

public void Update(string id, Book bookIn) =>


_books.ReplaceOne(book => book.Id == id, bookIn);

public void Remove(Book bookIn) =>


_books.DeleteOne(book => book.Id == bookIn.Id);

public void Remove(string id) =>


_books.DeleteOne(book => book.Id == id);
}
}

En el código anterior, se recuperó una instancia de IBookstoreDatabaseSettings de la inserción de


dependencias mediante la inserción de un constructor. Esta técnica proporciona acceso a los valores de
configuración de appsettings.json que se agregaron en la sección Adición de un modelo de configuración.
3. Agregue el código resaltado siguiente a Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddSingleton<BookService>();

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

En el código anterior, la clase BookService se registra con inserción de dependencias para admitir la
inserción del constructor en las clases de consumo. La duración de servicio de tipo singleton es la más
adecuada porque BookService toma una dependencia directa sobre MongoClient . Según las instrucciones
oficiales de reutilización de cliente Mongo, MongoClient debe registrarse en la inserción de dependencias
con una duración de servicio de tipo singleton.
4. Agregue el código siguiente en la parte superior del archivo Startup.cs para resolver la referencia a
BookService :

using BooksApi.Services;

La clase BookService usa los miembros MongoDB.Driver siguientes para realizar operaciones CRUD en la base de
datos:
MongoClient: lee la instancia del servidor para realizar operaciones de base de datos. Se proporciona la
cadena de conexión de MongoDB al constructor de esta clase:

public BookService(IBookstoreDatabaseSettings settings)


{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);

_books = database.GetCollection<Book>(settings.BooksCollectionName);
}

IMongoDatabase: representa la base de datos de Mongo para realizar operaciones. Este tutorial usa el
método genérico GetCollection<TDocument>(collection) en la interfaz para tener acceso a los datos de una
colección específica. Realice las operaciones CRUD en la colección después de llamar a este método. En la
llamada al método GetCollection<TDocument>(collection) :
collection representa el nombre de la colección.
TDocument representa el tipo de objeto CLR almacenado en la colección.
GetCollection<TDocument>(collection) devuelve un objeto MongoCollection que representa la colección. En este
tutorial, se invocan los métodos siguientes en la colección:
DeleteOne: elimina un único documento que cumpla los criterios de búsqueda proporcionados.
Find<TDocument>: devuelve todos los documentos de la colección que cumplen los criterios de búsqueda
indicados.
InsertOne: inserta el objeto proporcionado como un nuevo documento en la colección.
ReplaceOne: reemplaza un único documento que cumpla los criterios de búsqueda indicados por el objeto
proporcionado.

Incorporación de un controlador
Agregue una clase BooksController al directorio Controladores con el código siguiente:

using BooksApi.Models;
using BooksApi.Services;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace BooksApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
{
private readonly BookService _bookService;

public BooksController(BookService bookService)


{
_bookService = bookService;
}

[HttpGet]
public ActionResult<List<Book>> Get() =>
_bookService.Get();

[HttpGet("{id:length(24)}", Name = "GetBook")]


public ActionResult<Book> Get(string id)
{
var book = _bookService.Get(id);

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

return book;
}

[HttpPost]
public ActionResult<Book> Create(Book book)
{
_bookService.Create(book);

return CreatedAtRoute("GetBook", new { id = book.Id.ToString() }, book);


}

[HttpPut("{id:length(24)}")]
public IActionResult Update(string id, Book bookIn)
{
var book = _bookService.Get(id);

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

_bookService.Update(id, bookIn);

return NoContent();
}

[HttpDelete("{id:length(24)}")]
public IActionResult Delete(string id)
{
var book = _bookService.Get(id);

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

_bookService.Remove(book.Id);

return NoContent();
}
}
}

El controlador de API web anterior:


Usa la clase BookService para realizar operaciones CRUD.
Contiene métodos de acción para admitir las solicitudes GET, POST, PUT y DELETE de HTTP.
Llama a CreatedAtRoute en el método de acción Create para devolver una respuesta HTTP 201. El código de
estado 201 es la respuesta estándar para un método HTTP POST que crea un recurso en el servidor.
CreatedAtRoute también agrega un encabezado Location a la respuesta. El encabezado Location especifica el
identificador URI del libro recién creado.

Prueba de la API web


1. Compile y ejecute la aplicación.
2. Vaya a http://localhost:<port>/api/books para probar el método de acción Get sin parámetros del
controlador. Se muestra la siguiente respuesta JSON:

[
{
"id":"5bfd996f7b8e48dc15ff215d",
"bookName":"Design Patterns",
"price":54.93,
"category":"Computers",
"author":"Ralph Johnson"
},
{
"id":"5bfd996f7b8e48dc15ff215e",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}
]

3. Vaya a http://localhost:<port>/api/books/5bfd996f7b8e48dc15ff215e para probar el método de acción Get


sobrecargado del controlador. Se muestra la siguiente respuesta JSON:

{
"id":"5bfd996f7b8e48dc15ff215e",
"bookName":"Clean Code",
"price":43.15,
"category":"Computers",
"author":"Robert C. Martin"
}

Configuración de las opciones de serialización de JSON


Hay dos detalles que cambiar sobre las respuestas JSON devueltas en la sección Prueba de la API web:
Las mayúsculas y minúsculas Camel predeterminadas de los nombres de propiedad se deben cambiar para que
coincidan con el uso de mayúsculas y minúsculas de Pascal de los nombres de propiedad del objeto CLR.
La propiedad bookName se debe devolver como Name .

Para satisfacer los requisitos anteriores, realice los cambios siguientes:


1. En Startup.ConfigureServices , cambie el código resaltado siguiente en la llamada al método AddMvc :
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookstoreDatabaseSettings>(
Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

services.AddSingleton<BookService>();

services.AddMvc()
.AddJsonOptions(options => options.UseMemberCasing())
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Con el cambio anterior, los nombres de propiedad de la respuesta JSON serializada de la API web coinciden
con sus nombres de propiedad correspondientes en el tipo de objeto CLR. Por ejemplo, la propiedad
Author de la clase Book se serializa como Author .

2. En Models/Book.cs, anote la propiedad BookName con el atributo [JsonProperty] siguiente:

[BsonElement("Name")]
[JsonProperty("Name")]
public string BookName { get; set; }

El valor Name del atributo [JsonProperty] representa el nombre de propiedad en la respuesta JSON
serializada de la API web.
3. Agregue el código siguiente en la parte superior del archivo Models/Book.cs para resolver la referencia al
atributo [JsonProperty] :

using Newtonsoft.Json;

4. Repita los pasos definidos en la sección Prueba de la API web. Observe la diferencia en los nombres de
propiedad JSON.

Pasos siguientes
Para obtener más información sobre la creación de las API web de ASP.NET Core, consulte los siguientes recursos:
Versión de YouTube de este artículo
Creación de API web con ASP.NET Core
Tipos de valor devuelto de acción del controlador de ASP.NET Core Web API
Páginas de ayuda de ASP.NET Core Web API con
Swagger/Open API
27/06/2019 • 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 OpenAPI, 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 para generar documentos de Swagger y que sirve para
integrar la interfaz de usuario de Swagger o ReDoc en las API web de ASP.NET Core. De manera
opcional, NSwag ofrece métodos para generar código de cliente de C# y TypeScript para la API.

¿Qué es Swagger/OpenAPI?
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 ahora se conoce como OpenAPI. Ambos nombres se usan
indistintamente, aunque se prefiere OpenAPI. 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)
Introducción a Swashbuckle y ASP.NET Core
02/07/2019 • 21 minutes to read • Edit Online

Por Shayne Boyer y Scott Addie


Vea o descargue el código de ejemplo (cómo descargarlo)
Hay tres componentes principales de Swashbuckle:
Swashbuckle.AspNetCore.Swagger: modelo de objetos de Swagger y middleware para exponer objetos
SwaggerDocument como puntos de conexión JSON.

Swashbuckle.AspNetCore.SwaggerGen: generador de Swagger que genera objetos SwaggerDocument


directamente desde las rutas, controladores y modelos. Se suele combinar con el middleware de punto de
conexión de Swagger para exponer automáticamente el JSON de Swagger.
Swashbuckle.AspNetCore.SwaggerUI: versión insertada de la herramienta de interfaz de usuario de
Swagger. Interpreta el JSON de Swagger para crear una experiencia enriquecida y personalizable para
describir la funcionalidad de la API web. Incluye herramientas de ejecución de pruebas integradas para los
métodos públicos.

Instalación del paquete


Se puede agregar Swashbuckle con los métodos siguientes:
Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
En la ventana Consola del Administrador de paquetes:
Vaya a Vista > Otras ventanas > Consola del Administrador de paquetes.
Vaya al directorio en el que está TodoApi.csproj.
Ejecute el siguiente comando:

Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc2

En el cuadro de diálogo Administrar paquetes NuGet:


Haga clic con el botón derecho en el proyecto en el Explorador de soluciones > Administrar
paquetes NuGet.
Establezca el origen del paquete en "nuget.org".
Escriba "Swashbuckle.AspNetCore" en el cuadro de búsqueda.
Seleccione el paquete "Swashbuckle.AspNetCore" en la pestaña Examinar y haga clic en Instalar.

Agregar y configurar el middleware de Swagger


En la clase Startup , importe el siguiente espacio de nombres para que use la clase OpenApiInfo :
using Microsoft.OpenApi.Models;

Agregue el generador de Swagger a la colección de servicios en el método Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
}

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
}

En el método Startup.Configure , habilite el middleware para servir el documento JSON generado y la interfaz de
usuario de Swagger:

public void Configure(IApplicationBuilder app)


{
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),


// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

app.UseMvc();
}

La llamada de método UseSwaggerUI anterior habilita el middleware de archivos estáticos. Si el destino es .NET
Framework o .NET Core 1.x, agregue el paquete NuGet Microsoft.AspNetCore.StaticFiles al proyecto.
Inicie la aplicación y vaya a http://localhost:<port>/swagger/v1/swagger.json . El documento generado en el que se
describen los puntos de conexión aparecerá según se muestra en la especificación de Swagger (swagger.json).
La interfaz de usuario de Swagger se encuentra en http://localhost:<port>/swagger . Explore la API a través de la
interfaz de usuario de Swagger e incorpórela a otros programas.
TIP
Para servir la interfaz de usuario de Swagger en la raíz de la aplicación ( http://localhost:<port>/ ), establezca la
propiedad RoutePrefix en una cadena vacía:

app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = string.Empty;
});

Si usa directorios con IIS o un proxy inverso, establezca el punto de conexión de Swagger en una ruta de acceso
relativa mediante el prefijo ./ . Por ejemplo: ./swagger/v1/swagger.json . El uso de /swagger/v1/swagger.json
indica a la aplicación que debe buscar el archivo JSON en la verdadera raíz de la dirección URL (junto con el
prefijo de ruta, si se usa). Por ejemplo, use http://localhost:<port>/<route_prefix>/swagger/v1/swagger.json en
lugar de http://localhost:<port>/<virtual_directory>/<route_prefix>/swagger/v1/swagger.json .

Personalizar y ampliar
Swagger proporciona opciones para documentar el modelo de objetos y personalizar la interfaz de usuario para
que coincida con el tema.
Información y descripción de la API
La acción de configuración que se pasa al método AddSwaggerGen agrega información, como el autor, la licencia y
la descripción:

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});
});

La interfaz de usuario de Swagger muestra información de la versión:


comentarios XML
Los comentarios XML se pueden habilitar con los métodos siguientes:
Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
Haga clic con el botón derecho en el Explorador de soluciones y seleccione Editar
<nombre_de_proyecto>.csproj.
Agregue manualmente las líneas resaltadas al archivo .csproj:

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Haga clic con el botón derecho en el proyecto en el Explorador de soluciones y seleccione Propiedades.
Active la casilla Archivo de documentación XML en la sección Salida de la pestaña Compilar.
Puede habilitar los comentarios XML para proporcionar información de depuración para miembros y tipos
públicos sin documentación. Los miembros y tipos que no estén documentados se indican por medio del mensaje
de advertencia. Por ejemplo, el siguiente mensaje señala una infracción con el código de advertencia 1591:

warning CS1591: Missing XML comment for publicly visible type or member 'TodoController.GetAll()'

Para eliminar las advertencias a nivel de proyecto, defina una lista delimitada por punto y coma de códigos de
advertencia que se deban omitir en el archivo de proyecto. Al anexar los códigos de advertencia a $(NoWarn); , se
aplican también los valores predeterminados de C#.

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Para suprimir las advertencias solo para miembros específicos, incluya el código en directivas de preprocesador
de advertencias #pragma. Este enfoque es útil para el código que no debe exponerse mediante los documentos de
API. En el ejemplo siguiente, se omite el código de advertencia CS1591 para toda la clase Program . La ejecución
del código de advertencia se restaura al cerrar la definición de clase. Especifique varios códigos de advertencia
con una lista delimitada por comas.

namespace TodoApi
{
#pragma warning disable CS1591
public class Program
{
public static void Main(string[] args) =>
BuildWebHost(args).Run();

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
#pragma warning restore CS1591
}

Configure Swagger para usar el archivo XML que se generó con las instrucciones anteriores. En Linux o sistemas
operativos que no sean Windows, las rutas de acceso y los nombres de archivo pueden distinguir entre
mayúsculas y minúsculas. Por ejemplo, un archivo TodoApi.XML es válido en Windows, pero no en CentOS.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});

// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});

// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();

// Register the Swagger generator, defining 1 or more Swagger documents


services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});

// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetEntryAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}

En el código anterior, Reflection se usa para crear un nombre de archivo XML que coincida con el proyecto de API
web. La propiedad AppContext.BaseDirectory sirve para crear una ruta de acceso al archivo XML. Algunas
características de Swagger (por ejemplo, esquemas de parámetros de entrada o métodos HTTP y códigos de
respuesta de los atributos respectivos) funcionan sin el uso de un archivo de documentación XML. Para la mayoría
de las características, es decir, los resúmenes de los métodos y las descripciones de los parámetros y los códigos
de respuesta, el uso de un archivo XML es obligatorio.
Agregar comentarios con la triple barra diagonal a una acción mejora la interfaz de usuario de Swagger en tanto
agrega una descripción de la acción al encabezado de la sección. Agregue un elemento <summary> antes de la
acción Delete :
/// <summary>
/// Deletes a specific TodoItem.
/// </summary>
/// <param name="id"></param>
[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 interfaz de usuario de Swagger muestra el texto interno del elemento <summary> del código anterior:

La interfaz de usuario se controla por medio del esquema JSON generado:


"delete": {
"tags": [
"Todo"
],
"summary": "Deletes a specific TodoItem.",
"operationId": "ApiTodoByIdDelete",
"consumes": [],
"produces": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"200": {
"description": "Success"
}
}
}

Agregue un elemento <remarks> a la documentación del método de acción Create . Este elemento complementa
la información especificada en el elemento <summary> y proporciona una interfaz de usuario de Swagger más
sólida. El contenido del elemento <remarks> puede estar formado por texto, JSON o XML.

/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(typeof(TodoItem), 201)]
[ProducesResponseType(400)]
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);


}
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

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


}

Fíjese en las mejoras de la interfaz de usuario con estos comentarios extra:

Anotaciones de datos
Incorpore al modelo atributos que se encuentren en System.ComponentModel.DataAnnotations para controlar
los componentes de la interfaz de usuario de Swagger.
Agregue el atributo [Required] a la propiedad Name de la clase TodoItem :
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }

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

[DefaultValue(false)]
public bool IsComplete { get; set; }
}
}

La presencia de este atributo cambia el comportamiento de la interfaz de usuario y modifica el esquema JSON
subyacente:

"definitions": {
"TodoItem": {
"required": [
"name"
],
"type": "object",
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"isComplete": {
"default": false,
"type": "boolean"
}
}
}
},

Agregue el atributo [Produces("application/json")] al controlador de API. Su propósito consiste en declarar que


las acciones del controlador admiten un contenido de respuesta de tipo application/json:

[Produces("application/json")]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;

En el menú desplegable Response Content Type (Tipo de contenido de respuesta) se selecciona este tipo de
contenido como el valor predeterminado para las acciones GET del controlador:
A medida que aumenta el uso de anotaciones de datos en la API web, la interfaz de usuario y las páginas de ayuda
de la API pasan a ser más descriptivas y útiles.
Describir los tipos de respuesta
Lo que más preocupa a los desarrolladores que consumen una API web es lo que se devuelve; sobre todo, los
tipos de respuesta y los códigos de error (si no son los habituales). Los tipos de respuesta y los códigos de error se
indican en las anotaciones de datos y los comentarios XML.
La acción Create devuelve un código de estado HTTP 201 cuando se ha ejecutado correctamente. Cuando el
cuerpo de solicitud enviado es null, se devuelve un código de estado HTTP 400. Sin la documentación correcta en
la interfaz de usuario de Swagger, el consumidor no dispone de la información necesaria de estos resultados
esperados. Corrija este problema agregando las líneas resaltadas en el siguiente ejemplo:

/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(typeof(TodoItem), 201)]
[ProducesResponseType(400)]
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);


}
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();

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


}

Ahora, la interfaz de usuario de Swagger documenta de forma clara los códigos de respuesta HTTP esperados:

En ASP.NET Core 2.2 o versiones posteriores, las convenciones se pueden usar como alternativa a la
representación explícita de acciones individuales con [ProducesResponseType] . Para más información, consulte Uso
de convenciones de API web.
Personalizar la interfaz de usuario
La interfaz de usuario es funcional y tiene un aspecto adecuado. Pero las páginas de documentación de la API
deben ostentar su marca o tema. Para incluir la personalización de marca en los componentes de Swashbuckle, se
deben agregar los recursos para servir archivos estáticos y generar la estructura de carpetas que hospedará estos
archivos.
Si el destino es .NET Framework o .NET Core 1.x, agregue el paquete NuGet Microsoft.AspNetCore.StaticFiles al
proyecto:

<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />

El paquete NuGet anterior ya estará instalado si el destino es .NET Core 2.x y se usa el metapaquete.
Habilitar middleware de archivos estáticos:

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles();

// Enable middleware to serve generated Swagger as a JSON endpoint.


app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),


// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

app.UseMvc();
}

Obtenga el contenido de la carpeta dist en el repositorio de GitHub de la interfaz de usuario de Swagger. Esta
carpeta contiene los recursos necesarios para la página de interfaz de usuario de Swagger.
Cree una carpeta wwwroot/swagger/ui y copie en ella el contenido de la carpeta dist.
Cree un archivo custom.css en wwwroot/swagger/ui con el siguiente código CSS para personalizar el encabezado
de página:

.swagger-ui .topbar {
background-color: #000;
border-bottom: 3px solid #547f00;
}

Cree una referencia a custom.css en el archivo index.html después de cualquier otro archivo CSS:

<link href="https://fonts.googleapis.com/css?
family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css">
<link rel="stylesheet" type="text/css" href="custom.css">

Vaya a la página index.html en http://localhost:<port>/swagger/ui/index.html. Escriba


http://localhost:<port>/swagger/v1/swagger.json en el cuadro de texto del encabezado y haga clic en el botón
Explorar. La página resultante tiene el siguiente aspecto:
Puede hacer muchas más cosas con la página. Vea todas las capacidades de los recursos de la interfaz de usuario
en el repositorio de GitHub de la interfaz de usuario de Swagger.
Introducción a NSwag y ASP.NET Core
02/07/2019 • 12 minutes to read • Edit Online

Por Christoph Nienaber, Rico Suter y Dave Brock


Vea o descargue el código de ejemplo (cómo descargarlo)
Vea o descargue el código de ejemplo (cómo descargarlo)
NSwag ofrece las siguientes capacidades:
Capacidad de usar la interfaz de usuario y el generador de Swagger.
Capacidad de generar código con flexibilidad.
Con NSwag, no es necesario que exista una API; se pueden usar API de terceros que incluyan Swagger y que
generen una implementación de cliente. NSwag permite acelerar el ciclo de desarrollo y adaptarse fácilmente a los
cambios de API.

Registro del middleware de NSwag


Registre el middleware de NSwag para:
Generar la especificación de Swagger para la API web implementada.
Proporcionar la interfaz de usuario de Swagger para examinar y probar la API web.
Para usar NSwag con middleware de ASP.NET Core, instale el paquete NuGet NSwag.AspNetCore. Este paquete
contiene el middleware para generar y proporcionar la especificación de Swagger, la interfaz de usuario de
Swagger (v2 y v3) y la interfaz de usuario de ReDoc.
Use uno de los siguientes métodos para instalar el paquete NuGet de NSwag:
Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
En la ventana Consola del Administrador de paquetes:
Vaya a Vista > Otras ventanas > Consola del Administrador de paquetes.
Vaya al directorio en el que está TodoApi.csproj.
Ejecute el siguiente comando:

Install-Package NSwag.AspNetCore

En el cuadro de diálogo Administrar paquetes NuGet:


Haga clic con el botón derecho en el proyecto en el Explorador de soluciones > Administrar
paquetes NuGet.
Establezca el origen del paquete en "nuget.org".
Escriba "NSwag.AspNetCore" en el cuadro de búsqueda.
Seleccione el paquete "NSwag.AspNetCore" en la pestaña Examinar y haga clic en Instalar.
Agregar y configurar el middleware de Swagger
Para agregar y configurar Swagger en su aplicación de ASP.NET Core, realice los siguientes pasos:
En el método Startup.ConfigureServices , registre los servicios necesarios de Swagger:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();

// Register the Swagger services


services.AddSwaggerDocument();
}

En el método Startup.Configure , habilite el middleware para servir la especificación y la interfaz de usuario de


Swagger:

public void Configure(IApplicationBuilder app)


{
app.UseStaticFiles();

// Register the Swagger generator and the Swagger UI middlewares


app.UseOpenApi();
app.UseSwaggerUi3();

app.UseMvc();
}

Inicie la aplicación. Vaya a:


http://localhost:<port>/swagger para ver la interfaz de usuario de Swagger.
http://localhost:<port>/swagger/v1/swagger.json para ver la especificación de Swagger.

Generación de código
Para aprovechar las capacidades de generación de código de NSwag, elija una de las siguientes opciones:
NSwagStudio, una aplicación de escritorio de Windows que permite generar código de cliente de API de C# o
TypeScript.
Paquetes NuGet NSwag.CodeGeneration.CSharp o NSwag.CodeGeneration.TypeScript para generar código
dentro del proyecto.
NSwag desde la línea de comandos.
Paquete NuGet NSwag.MSBuild.
Servicio conectado de Unchase OpenAPI (Swagger), un servicio conectado de Visual Studio para generar
código de cliente de API en C# o TypeScript. También genera controladores de C# para los servicios de
OpenAPI con NSwag.
Generación de código con NSwagStudio
Instale NSwagStudio siguiendo las instrucciones del repositorio de GitHub de NSwagStudio.
Inicie NSwagStudio y escriba la URL del archivo swagger.json en el cuadro de texto Swagger Specification
URL (URL de especificación de Swagger). Por ejemplo, http://localhost:44354/swagger/v1/swagger.json .
Haga clic en el botón Create local Copy (Crear copia local) para generar una representación JSON de la
especificación de Swagger.
En el área Outputs (Salidas), haga clic en la casilla CSharp Client (Cliente de CSharp). En función del
proyecto, también puede elegir TypeScript Client (Cliente de TypeScript) o CSharp Web API Controller
(Controlador de API web de CSharp). Si selecciona CSharp Web API Controller (Controlador de API web
de CSharp), una especificación de servicio volverá a generar el servicio a modo de generación inversa.
Haga clic en Generate Outputs (Generar salidas) para producir una implementación completa del cliente
de C# del proyecto TodoApi.NSwag. Para ver el código de cliente generado, haga clic en la pestaña CSharp
Client (Cliente de CSharp):
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v12.0.9.0 (NJsonSchema v9.13.10.0 (Newtonsoft.Json v11.0.0.0))
(http://NSwag.org)
// </auto-generated>
//----------------------

namespace MyNamespace
{
#pragma warning disable

[System.CodeDom.Compiler.GeneratedCode("NSwag", "12.0.9.0 (NJsonSchema v9.13.10.0 (Newtonsoft.Json


v11.0.0.0))")]
public partial class TodoClient
{
private string _baseUrl = "https://localhost:44354";
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;

public TodoClient(System.Net.Http.HttpClient httpClient)


{
_httpClient = httpClient;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}

public string BaseUrl


{
get { return _baseUrl; }
set { _baseUrl = value; }
}

// code omitted for brevity

TIP
El código de cliente de C# se genera en función de las selecciones en la pestaña Settings (Configuración). Modifique esta
configuración para realizar tareas como cambiar el nombre del espacio de nombres predeterminado y generar un método
sincrónico.

Copie el código de C# generado en un archivo del proyecto de cliente que usará la API.
Empiece a consumir la Web API:

var todoClient = new TodoClient();

// Gets all to-dos from the API


var allTodos = await todoClient.GetAllAsync();

// Create a new TodoItem, and save it via the API.


var createdTodo = await todoClient.CreateAsync(new TodoItem());

// Get a single to-do by ID


var foundTodo = await todoClient.GetByIdAsync(1);

Personalización de la documentación de API


Swagger proporciona opciones para documentar el modelo de objetos de cara a facilitar el consumo de Web API.
Información y descripción de la API
En el método Startup.ConfigureServices , la acción de configuración que se pasa al método AddSwaggerDocument
agrega información, como el autor, la licencia y la descripción:

services.AddSwaggerDocument(config =>
{
config.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "ToDo API";
document.Info.Description = "A simple ASP.NET Core web API";
document.Info.TermsOfService = "None";
document.Info.Contact = new NSwag.OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = "https://twitter.com/spboyer"
};
document.Info.License = new NSwag.OpenApiLicense
{
Name = "Use under LICX",
Url = "https://example.com/license"
};
};
});

La interfaz de usuario de Swagger muestra información de la versión:

comentarios XML
Para habilitar los comentarios XML, realice los siguientes pasos:
Visual Studio
Visual Studio para Mac
Visual Studio Code y CLI de .NET Core
Haga clic con el botón derecho en el Explorador de soluciones y seleccione Editar
<nombre_de_proyecto>.csproj.
Agregue manualmente las líneas resaltadas al archivo .csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Propiedades.
Active la casilla Archivo de documentación XML en la sección Salida de la pestaña Compilar.
Anotaciones de datos
Dado que NSwag usa Reflection y el tipo de valor devuelto recomendado para acciones de API web es
IActionResult, no puede inferir su acción ni el valor que devuelve.
Considere el ejemplo siguiente:

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


}

La acción anterior devuelve IActionResult pero, dentro de la acción, devuelve CreatedAtRoute o BadRequest. Use
las anotaciones de datos para indicar a los clientes qué códigos de estado HTTP se sabe que esta acción devuelve.
Complete la acción con los siguientes atributos:

[ProducesResponseType(typeof(TodoItem), 201)] // Created


[ProducesResponseType(400)] // BadRequest

Dado que NSwag usa Reflection y el tipo de valor devuelto recomendado para acciones de API web es
ActionResult<T>, solo puede inferir el tipo de valor devuelto definido por T . No puede inferir automáticamente
ningún otro tipo de valor devuelto.
Considere el ejemplo siguiente:

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

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


}

La acción anterior devuelve ActionResult<T> . Dentro de la acción, se devuelve CreatedAtRoute. Como el


controlador se representa con el atributo [ApiController], también es posible una respuesta BadRequest. Para
obtener más información, consulte Respuestas HTTP 400 automáticas. Use las anotaciones de datos para indicar a
los clientes qué códigos de estado HTTP se sabe que esta acción devuelve. Complete la acción con los siguientes
atributos:
[ProducesResponseType(201)] // Created
[ProducesResponseType(400)] // BadRequest

En ASP.NET Core 2.2 o versiones posteriores, puede usar las convenciones en lugar de decorar explícitamente
acciones individuales con [ProducesResponseType] . Para más información, consulte Uso de convenciones de API
web.
Ahora, el generador de Swagger puede describir con precisión esta acción y los clientes generados sabrán lo que
reciben cuando llamen al punto de conexión. Le recomendamos que decore todas las acciones con estos atributos.
Para obtener instrucciones sobre qué respuestas HTTP deben devolver las acciones de API, vea la especificación
RFC 7231.
Tipos de valor devuelto de acción del controlador de
ASP.NET Core Web API
06/06/2019 • 10 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(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
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 del asistente 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 del asistente 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(typeof(Product), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync([FromBody] Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

await _repository.AddProductAsync(product);

return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);


}

En el código anterior:
El entorno de ejecución de ASP.NET Core devuelve un código de estado 400 (BadRequest) cuando la
descripción del producto contiene "Widget XYZ".
Al crear un producto, el método CreatedAtAction genera un código de estado 201. En esta ruta de acceso de
código, se devuelve el objeto Product .
Por ejemplo, el siguiente modelo indica que las solicitudes deben incluir las propiedades Name y Description .
Por lo tanto, si no se proporcionan Name y Description en la solicitud, se producirá un error en la validación de
los modelos.
public class Product
{
public int Id { get; set; }

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

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

En ASP.NET Core 2.1 o versiones posteriores, si se aplica el atributo [ApiController], los errores de validación de
los modelos generarán un código de estado 400. Para obtener más información, consulte Respuestas HTTP 400
automáticas.

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(StatusCodes.Status404NotFound)]
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(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync(Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

await _repository.AddProductAsync(product);

return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);


}

En el código anterior:
El entorno de ejecución de ASP.NET Core devuelve un código de estado 400 (BadRequest) en los casos
siguientes:
El atributo [ApiController] se ha aplicado y se produce un error de validación de los modelos.
La descripción del producto contiene "Widget XYZ".
Al crear un producto, el método CreatedAtAction genera un código de estado 201. En esta ruta de acceso de
código, se devuelve el objeto 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] . 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
Aplicación de formato a datos de respuesta en
ASP.NET Core Web API
31/05/2019 • 18 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 del asistente 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 del asistente 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 del asistente 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 del asistente (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 del asistente 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.
Configuración de formateadores basados en System.Text.Json
Las características para los formateadores basados en System.Text.Json pueden configurarse mediante
Microsoft.AspNetCore.Mvc.MvcOptions.SerializerOptions .

services.AddMvc(options =>
{
options.SerializerOptions.WriterSettings.Indented = true;
});

Adición de compatibilidad con el formato JSON basado en Newtonsoft.Json


Antes de ASP.NET Core 3.0, MVC usa de forma predeterminada formateadores JSON que se implementan
mediante el paquete Newtonsoft.Json . En ASP.NET Core 3.0 o posterior, los formateadores JSON
predeterminados se basan en System.Text.Json . La compatibilidad con los formateadores basados en
Newtonsoft.Json y con las características está disponible al instalar el paquete NuGet
Microsoft.AspNetCore.Mvc.NewtonsoftJson y configurarlo en Startup.ConfigureServices .
services.AddMvc()
.AddNewtonsoftJson();

Es posible que algunas características no funcionen bien con formateadores basados en System.Text.Json y
requieren una referencia a los formateadores basados en Newtonsoft.Json para la versión de ASP.NET Core 3.0.
Siga usando los formateadores basados en Newtonsoft.Json si la aplicación ASP.NET Core 3.0 o posterior:
Usa atributos Newtonsoft.Json (por ejemplo, [JsonProperty] o [JsonIgnore] ), personaliza la configuración
de serialización o se basa en características que Newtonsoft.Json proporciona.
Configura Microsoft.AspNetCore.Mvc.JsonResult.SerializerSettings . Antes de ASP.NET Core 3.0,
JsonResult.SerializerSettings acepta una instancia de JsonSerializerSettings específica de
Newtonsoft.Json .
Genera la documentación de OpenAPI.
Adición de compatibilidad con el formato XML
Para agregar compatibilidad con el formato XML en ASP.NET Core 2.2 o anterior, instale el paquete NuGet
Microsoft.AspNetCore.Mvc.Formatters.Xml.
Los formateadores XML que se implementan mediante System.Xml.Serialization.XmlSerializer pueden
configurarse llamando a AddXmlSerializerFormatters en Startup.ConfigureServices :

services.AddMvc()
.AddXmlSerializerFormatters();

Además, los formateadores XML que se implementan mediante


System.Runtime.Serialization.DataContractSerializer pueden configurarse llamando a
AddXmlDataContractSerializerFormatters en Startup.ConfigureServices :

services.AddMvc()
.AddXmlDataContractSerializerFormatters();

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

/products/GetById/5.json Formateador JSON (si está configurado)

/products/GetById/5.xml Formateador XML (si está configurado)


Formateadores personalizados en ASP.NET Core
Web API
10/05/2019 • 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 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 y XML ).
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 obtener un ejemplo del formateador de entrada, consulte la aplicación de ejemplo.
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);
}

Para obtener un ejemplo del formateador de entrada, consulte la aplicación de ejemplo.

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

Para obtener un ejemplo del formateador de entrada, consulte la aplicación de ejemplo.


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

Para obtener un ejemplo del formateador de entrada, consulte la aplicación de ejemplo.

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());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Los formateadores se evalúan en el orden en que se insertaron. El primero de ellos tiene prioridad.

Pasos siguientes
Código de ejemplo de formateador de texto sin formato en GitHub.
Aplicación de ejemplo de este documento, que implementa 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.
Uso de analizadores de API web
04/07/2019 • 3 minutes to read • Edit Online

ASP.NET Core 2.2 (y versiones posteriores) incluye el paquete NuGet Microsoft.AspNetCore.Mvc.Api.Analyzers,


que contiene analizadores para las API web. Los analizadores funcionan con los controladores anotados con
ApiControllerAttribute y se compilan con convenciones de la API.

Instalación del paquete


Se puede agregar Microsoft.AspNetCore.Mvc.Api.Analyzers con uno de los métodos siguientes:
Visual Studio
Visual Studio para Mac
Visual Studio Code
CLI de .NET Core
En la ventana Consola del Administrador de paquetes:
Vaya a Vista > Otras ventanas > Consola del Administrador de paquetes.
Vaya al directorio en el que esté el archivo ApiConventions.csproj.
Ejecute el siguiente comando:

Install-Package Microsoft.AspNetCore.Mvc.Api.Analyzers

En el cuadro de diálogo Administrar paquetes NuGet:


Haga clic con el botón derecho en el proyecto en el Explorador de soluciones > Administrar
paquetes NuGet.
Establezca el origen del paquete en "nuget.org".
En el cuadro de búsqueda, escriba "Microsoft.AspNetCore.Mvc.Api.Analyzers".
Seleccione el paquete "Microsoft.AspNetCore.Mvc.Api.Analyzers" en la pestaña Examinar y haga clic en
Instalar.

Analizadores para las convenciones de API


Los documentos de Open API contienen los códigos de estado y los tipos de respuesta que puede devolver una
acción. En ASP.NET Core MVC, los atributos como ProducesResponseTypeAttribute y ProducesAttribute se usan
para documentar una acción. Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API ofrece
información más detallada sobre cómo documentar su API.
Uno de los analizadores del paquete inspecciona los controladores anotados con ApiControllerAttribute e
identifica las acciones que no documentan completamente sus respuestas. Considere el ejemplo siguiente:
// GET api/contacts/{guid}
[HttpGet("{id}", Name = "GetById")]
[ProducesResponseType(typeof(Contact), StatusCodes.Status200OK)]
public IActionResult Get(string id)
{
var contact = _contacts.Get(id);

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

return Ok(contact);
}

La acción anterior documenta el tipo de valor devuelto correcto HTTP 200, pero no documenta el código de
estado de error HTTP 404. El analizador informa de la documentación que falta para el código de estado 404 de
HTTP como una advertencia. Se proporciona una opción para corregir el problema.

Recursos adicionales
Uso de convenciones de API web
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Creación de API web con ASP.NET Core
Uso de convenciones de API web
04/07/2019 • 7 minutes to read • Edit Online

Por Pranav Krishnamoorthy y Scott Addie


ASP.NET Core 2.2 (y versiones posteriores) incluye una forma de extraer la documentación de API común y
aplicarla a varias acciones o varios controladores, e incluso a todos los controladores dentro de un ensamblado.
Las convenciones de API web son un sustituto para complementar acciones individuales con
[ProducesResponseType].
Una convención permite lo siguiente:
Definir los tipos de valor devuelto más comunes y los códigos de estado devueltos a partir de un tipo
específico de acción.
Identificar las acciones que no siguen el estándar definido.
ASP.NET Core MVC 2.2 (y versiones posteriores) incluye un conjunto de convenciones predeterminadas en
Microsoft.AspNetCore.Mvc.DefaultApiConventions. Las convenciones se basan en el controlador
(ValuesController.cs) proporcionado en la plantilla de proyecto de la API de ASP.NET Core. Si sus acciones
siguen los patrones de la plantilla, debería poder usar las convenciones predeterminadas correctamente. Si las
convenciones predeterminadas no satisfacen sus necesidades, consulte Creación de convenciones de API web.
En tiempo de ejecución, Microsoft.AspNetCore.Mvc.ApiExplorer entiende las convenciones. ApiExplorer es la
abstracción de MVC para comunicarse con los generadores de documento de OpenAPI, conocido también como
Swagger. Los atributos de la convención aplicada se asocian a una acción y se incluyen en la documentación de
OpenAPI de la acción. Los analizadores de API también comprenden las convenciones. Si la acción no es
convencional (por ejemplo, devuelve un código de estado no documentado en la convención aplicada), recibirá
una advertencia en la que se le animará a documentar el código de estado.
Vea o descargue el código de ejemplo (cómo descargarlo)

Aplicación de convenciones de API web


Las convenciones no se crean, y es posible que cada acción esté asociada a una única convención. Las
convenciones más específicas tienen prioridad sobre las menos específicas. La selección es no determinista
cuando se aplican dos o más convenciones de la misma prioridad a una acción. Las siguientes opciones existen
para aplicar una convención a una acción, de la más específica a la menos específica:
1. Microsoft.AspNetCore.Mvc.ApiConventionMethodAttribute : se aplica a las acciones individuales y especifica el
tipo de convención y el método de la convención que se aplica.
En el ejemplo siguiente, el método de convención Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put del
tipo de convención predeterminada se aplica a la acción Update :
// PUT api/contactsconvention/{guid}
[HttpPut("{id}")]
[ApiConventionMethod(typeof(DefaultApiConventions),
nameof(DefaultApiConventions.Put))]
public IActionResult Update(string id, Contact contact)
{
var contactToUpdate = _contacts.Get(id);

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

_contacts.Update(contact);

return NoContent();
}

El método de convención Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put aplica los siguientes


atributos a la acción:

[ProducesDefaultResponseType]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]

Para obtener más información sobre [ProducesDefaultResponseType] , vea Default Response (Respuesta
predeterminada).
2. Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute aplicado a un controlador: se aplica el tipo de
convención especificado a todas las acciones del controlador. Un método de convención se representa con
sugerencias que determinan las acciones a las que este se aplica. Para obtener más información sobre las
sugerencias, consulte Creación de convenciones de API web.
En el ejemplo siguiente, el conjunto predeterminado de convenciones se aplica a todas las acciones de
ContactsConventionController:

[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("api/[controller]")]
public class ContactsConventionController : ControllerBase
{

3. Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute aplicado a un ensamblado: se aplica el tipo de


convención especificado a todos los controladores del ensamblado actual. Como recomendación, aplique
los atributos de nivel de ensamblado al archivo Startup.cs.
En el ejemplo siguiente, el conjunto predeterminado de convenciones se aplica a todos los controladores
del ensamblado:

[assembly: ApiConventionType(typeof(DefaultApiConventions))]
namespace ApiConventions
{
public class Startup
{

Creación de convenciones de API web


Si las convenciones de API predeterminadas no satisfacen sus necesidades, cree sus propias convenciones. Una
convención es:
Un tipo estático con métodos.
Capaz de definir tipos de respuesta y requisitos de nomenclatura en acciones.
Tipos de respuesta
Estos métodos se anotan con atributos [ProducesResponseType] o [ProducesDefaultResponseType] . Por ejemplo:

public static class MyAppConventions


{
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public static void Find(int id)
{
}
}

Si no se presentan atributos de metadatos más específicos, la aplicación de esta convención a un ensamblado


requiere lo siguiente:
El método de convención debe aplicarse a cualquier acción denominada Find .
Un parámetro denominado id debe estar presente en la acción Find .
Requisitos de nomenclatura
Los atributos [ApiConventionNameMatch] y [ApiConventionTypeMatch] pueden aplicarse al método de convención
que determina las acciones a las que se aplican. Por ejemplo:

[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Find(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
int id)
{ }

En el ejemplo anterior:
La opción Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Prefix aplicada al método
indica que la convención coincide con cualquier acción que vaya precedida de "Búsqueda". Entre los ejemplos
de acciones coincidentes se incluyen Find , FindPet y FindById .
El valor Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Suffix aplicado al parámetro
indica que la convención coincide con los métodos que tengan exactamente un parámetro que termine con el
identificador del sufijo. Entre los ejemplos se incluyen parámetros tales como id o petId .
ApiConventionTypeMatch puede aplicarse de forma similar a los tipos para restringir el tipo de parámetro. Un
argumento params[] indica los parámetros restantes que no tienen que coincidir explícitamente.

Recursos adicionales
Uso de analizadores de API web
Páginas de ayuda de ASP.NET Core Web API con Swagger/Open API
Introducción a ASP.NET Core SignalR
10/05/2019 • 4 minutes to read • Edit Online

¿Qué es SignalR?
ASP.NET Core SignalR es una biblioteca de código abierto que simplifica la adición de la funcionalidad web en
tiempo real a las aplicaciones. La funcionalidad web en tiempo real permite que el código del lado servidor
inserte contenido en los clientes al momento.
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
Plataformas compatibles con ASP.NET Core SignalR
10/05/2019 • 2 minutes to read • Edit Online

Requisitos del sistema de servidor


SignalR para ASP.NET Core es compatible con cualquier plataforma de servidor ASP.NET Core admite.

Cliente de JavaScript
El cliente JavaScript se ejecuta en NodeJS 8 y versiones posteriores y los siguientes exploradores:

EXPLORADOR VERSIÓN

Microsoft Edge actuales

Mozilla Firefox actuales

Google Chrome; incluye Android actuales

Safari; incluye iOS actuales

Microsoft Internet Explorer 11

Cliente .NET
El cliente.NET se ejecuta en cualquier plataforma compatible con ASP.NET Core. Por ejemplo, los desarrolladores
de Xamarin pueden usar SignalR para la creación de aplicaciones de Android con Xamarin.Android 8.4.0.1 y
versiones posteriores y en aplicaciones de iOS con Xamarin.iOS 11.14.0.4 y versiones posteriores.
Si el servidor ejecuta IIS, el transporte de WebSockets requiere IIS 8.0 o posterior en Windows Server 2012 o
posterior. Otros transportes se admiten en todas las plataformas.

Cliente de Java
El cliente Java admite Java 8 y versiones posteriores.

Clientes no compatibles
Los clientes siguientes están disponibles, pero son experimentales o no oficiales. Que no se admiten actualmente y
nunca pueden ser.
Cliente de C++
Cliente de SWIFT
Tutorial: Introducción a SignalR de ASP.NET Core
04/07/2019 • 12 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:
Cree un proyecto web.
Agregar la biblioteca cliente de SignalR.
Crear un concentrador de SignalR.
Configurar el proyecto para usar SignalR.
Agregar código que envía 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 version 15.9 or later with the ASP.NET and web development workload. You can use
Visual Studio 2019, but some project creation steps differ from what's shown in the tutorial.
.NET Core SDK 2.2 or later

WARNING
If you use Visual Studio 2017, see dotnet/sdk issue #3124 for information about .NET Core SDK versions that don't work
with Visual Studio.

Creación de un proyecto web


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.2 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
(CDN ) que puede entregar todo lo que encuentre en npm, el administrador de paquetes de 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.

Creación de un concentrador de 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.
Puede llamarse al método SendMessage mediante un cliente conectado para enviar un mensaje a todos los
clientes. El código de cliente de JavaScript que llama al método se muestra más adelante en el tutorial. El
código de SignalR es asincrónico para proporcionar la máxima escalabilidad.

Configuración de 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 de ASP.NET Core y a la


canalización de software intermedio.
Adición del 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();

//Disable send button until connection is established


document.getElementById("sendButton").disabled = true;

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().then(function(){
document.getElementById("sendButton").disabled = false;
}).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 mensaje.
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
En este tutorial ha aprendido a:
Crear un proyecto de aplicación web.
Agregar la biblioteca cliente de SignalR.
Crear un concentrador de SignalR.
Configurar el proyecto para usar SignalR.
Agregar código que usa el concentrador para enviar mensajes desde cualquier cliente a todos los clientes
conectados.
Para obtener más información sobre SignalR, vea la introducción:
Introducción a SignalR de ASP.NET Core
Uso de SignalR de ASP.NET Core con TypeScript y
Webpack
21/05/2019 • 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
Visual Studio
Visual Studio Code
Visual Studio 2019 con la carga de trabajo ASP.NET y desarrollo web
.NET Core SDK 2.2 o posterior
Node.js con npm

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


Visual Studio
Visual Studio Code
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.
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 seleccione Aceptar.
3. Seleccione .NET Core en la lista desplegable de plataforma de destino y ASP.NET Core 2.2 en la lista
desplegable del selector de plataforma. Seleccione la plantilla Vacía y 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@1.0.1 css-loader@2.1.0 html-webpack-plugin@4.0.0-beta.5 mini-css-
extract-plugin@0.5.0 ts-loader@5.3.3 typescript@3.3.3 webpack@4.29.3 webpack-cli@3.2.3

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.29.3" en
lugar de "webpack": "^4.29.3" . 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 hace 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(long 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
Visual Studio Code
1. Ejecute Webpack en modo release. Desde la ventana Consola del administrador de paquetes, ejecute el
comando siguiente en la raíz del proyecto. Si no está en la raíz del proyecto, escriba cd SignalRWebPack
antes de introducir el comando.

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
Usar concentradores de SignalR para ASP.NET Core
10/05/2019 • 13 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 Task SendMessage(string user, string message)
{
return Clients.All.SendAsync("ReceiveMessage", user, message);
}
}

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.
NOTE
Los concentradores son transitorios:
No almacene el estado en una propiedad de la clase de concentrador. Cada llamada al método de concentrador se
ejecuta en una nueva instancia de concentrador.
Use await al llamar a métodos asincrónicos que dependen del concentrador permanecer activo. Por ejemplo, un
método como Clients.All.SendAsync(...) puede producir un error si se llama sin await y el método de
concentrador se completa antes de SendAsync finaliza.

El objeto de contexto
El Hub clase tiene un Context propiedad que contiene las siguientes propiedades con información sobre la
conexión:

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

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:

PROPIEDAD 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 en todas las conexiones del grupo


especificado

GroupExcept Llama a un método en todas las conexiones en el grupo


especificado, excepto las conexiones especificadas

Groups Llama a un método en varios grupos de conexiones

OthersInGroup Llama a un método en un grupo de conexiones, excepto al


cliente que invoca el método de concentrador

User Llama a un método en todas las conexiones asociadas a un


usuario específico

Users Llama a un método en 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,
hay tres métodos de concentrador:
SendMessage envía un mensaje a todos los clientes conectados, mediante Clients.All .
SendMessageToCaller envía un mensaje de vuelta al llamador, mediante Clients.Caller .
SendMessageToGroups envía un mensaje a todos los clientes de la SignalR Users grupo.
public Task SendMessage(string user, string message)
{
return Clients.All.SendAsync("ReceiveMessage", user, message);
}

public Task SendMessageToCaller(string message)


{
return Clients.Caller.SendAsync("ReceiveMessage", message);
}

public Task SendMessageToGroup(string message)


{
return Clients.Group("SignalR Users").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 . Cualquier método definido en la
interfaz todavía se puede definir como asincrónicas. De hecho, cada uno de estos métodos debe devolver un
Task . Puesto que es una interfaz, no use el async palabra clave. Por ejemplo:

public interface IClient


{
Task ClientMethod();
}
NOTE
El Async sufijo no se quitará el nombre del método. A menos que se define el método de cliente con
.on('MyMethodAsync') , no debe usar MyMethodAsync como un nombre.

Cambiar el nombre de un método de concentrador


De forma predeterminada, un nombre de método de concentrador de servidor es el nombre del método. NET.
Sin embargo, puede usar el HubMethodName atributo para cambiar este comportamiento predeterminado y
especificar manualmente un nombre para el método. El cliente debería utilizar este nombre, en lugar del nombre
de método. NET, al invocar el método.

[HubMethodName("SendMessageToUser")]
public Task DirectMessage(string user, string message)
{
return Clients.User(user).SendAsync("ReceiveMessage", message);
}

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

Invalidar el OnDisconnectedAsync método virtual para realizar acciones cuando un cliente se desconecta. Si el
cliente se desconecta de forma intencionada (mediante una llamada a connection.stop() , por ejemplo), el
exception parámetro será null . Sin embargo, si el cliente se desconecta debido a un error (por ejemplo, un
error de red), el exception parámetro contendrá una excepción que describe el error.

public override async Task OnDisconnectedAsync(Exception exception)


{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnDisconnectedAsync(exception);
}

Control de 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));

Si su centro, produce una excepción, no se cierran las conexiones. De forma predeterminada, SignalR devuelve
un mensaje de error genérico al cliente. Por ejemplo:

Microsoft.AspNetCore.SignalR.HubException: An unexpected error occurred invoking 'MethodName' on the server.

Excepciones inesperadas suelen contengan información confidencial, como el nombre de un servidor de base de
datos en una excepción que se desencadena cuando se produce un error en la conexión de base de datos.
SignalR no expone estos mensajes de error detallados de forma predeterminada como medida de seguridad.
Consulte la artículo de consideraciones de seguridad para obtener más información sobre por qué se suprimen
los detalles de la excepción.
Si tiene una excepcional de condición hacer desea propagar al cliente, puede usar el HubException clase. Si
produce un HubException desde su método de concentrador SignalR le enviar todo el mensaje al cliente, sin
modificar.

public Task ThrowException()


{
throw new HubException("This error will be sent to the client!");
}

NOTE
SignalR solo envía el Message propiedad de la excepción al cliente. El seguimiento de pila y otras propiedades en la
excepción no están disponibles para el cliente.

Recursos relacionados
Introducción a ASP.NET Core SignalR
Cliente de JavaScript
Publicar en Azure
Enviar mensajes desde fuera de un concentrador
10/05/2019 • 3 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(async (context, next) =>
{
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.

Insertar un HubContext fuertemente tipados


Para insertar un HubContext fuertemente tipado, asegúrese de que hereda de su centro de Hub<T> . Insertar
mediante el IHubContext<THub, T> interfaz en lugar de IHubContext<THub> .

public class ChatController : Controller


{
public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)


{
_strongChatHubContext = chatHubContext;
}

public async Task SendMessage(string message)


{
await _strongChatHubContext.Clients.All.ReceiveMessage(message);
}
}

Recursos relacionados
Introducción
Concentradores
Publicar en Azure
Administrar usuarios y grupos en SignalR
10/05/2019 • 4 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);
}

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.
Para proteger el acceso a los recursos durante el uso de grupos, use autenticación y autorización funcionalidad en
ASP.NET Core. Si agrega solo los usuarios a un grupo cuando las credenciales son válidas para ese grupo, los
mensajes enviados a ese grupo sólo irá a los usuarios autorizados. Sin embargo, los grupos no son una
característica de seguridad. Notificaciones de autenticación tienen características que no los grupos, como la
revocación y caducidad. Si se revoca el permiso de un usuario para acceder al grupo, tiene detectar y quitar del
grupo manualmente.

NOTE
Los nombres de grupo distinguen entre mayúsculas y minúsculas.

Recursos relacionados
Introducción
Concentradores
Publicar en Azure
Consideraciones de diseño de API de SignalR
10/05/2019 • 4 minutes to read • Edit Online

Por Andrew Stanton-Nurse


Este artículo proporcionan instrucciones para la creación de API basadas en SignalR.

Usar parámetros de objeto personalizado para garantizar la


compatibilidad con versiones anteriores
Agregar parámetros a un método de concentrador SignalR (en el cliente o servidor) es un cambio brusco. Esto
significa que los clientes y servidores más antiguos se producirán errores al intentar invocar el método sin el
número apropiado de parámetros. Sin embargo, agregar propiedades a un parámetro de objeto personalizado es
no un cambio importante. Esto puede usarse para diseñar las API compatibles que sean resistentes a los cambios
en el cliente o el servidor.
Por ejemplo, considere la posibilidad de una API de servidor similar al siguiente:

public async Task<string> GetTotalLength(string param1)


{
return param1.Length;
}

El cliente de JavaScript llama a este método con invoke como sigue:

connection.invoke("GetTotalLength", "value1");

Si posteriormente agrega un segundo parámetro para el método de servidor, los clientes más antiguos no
proporcionan este valor de parámetro. Por ejemplo:

public async Task<string> GetTotalLength(string param1, string param2)


{
return param1.Length + param2.Length;
}

Cuando el cliente anterior intenta invocar este método, producirá un error similar al siguiente:

Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength' due to an error on the server.

En el servidor, verá un mensaje de registro similar al siguiente:

System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 2.

El cliente anterior sólo envía un parámetro, pero la API más reciente del servidor requiere dos parámetros. Usar
objetos personalizados como parámetros, proporciona más flexibilidad. Vamos a volver a diseñar la API original
para usar un objeto personalizado:
public class TotalLengthRequest
{
public string Param1 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)


{
return req.Param1.Length;
}

Ahora, el cliente utiliza un objeto para llamar al método:

connection.invoke("GetTotalLength", { param1: "value1" });

En lugar de agregar un parámetro, agregue una propiedad a la TotalLengthRequest objeto:

public class TotalLengthRequest


{
public string Param1 { get; set; }
public string Param2 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)


{
var length = req.Param1.Length;
if (req.Param2 != null)
{
length += req.Param2.Length;
}
return length;
}

Cuando el cliente anterior envía un solo parámetro, el archivo extra Param2 propiedad quedará null . Puede
detectar un mensaje enviado por un cliente anterior mediante la comprobación de la Param2 para null y aplicar
un valor predeterminado. Un nuevo cliente puede enviar los dos parámetros.

connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });

Esta misma técnica funciona para los métodos definidos en el cliente. Puede enviar un objeto personalizado desde
el lado del servidor:

public async Task Broadcast(string message)


{
await Clients.All.SendAsync("ReceiveMessage", new
{
Message = message
});
}

En el lado cliente, tener acceso a la Message propiedad en lugar de utilizar un parámetro:

connection.on("ReceiveMessage", (req) => {


appendMessageToChatWindow(req.message);
});

Si más adelante decide agregar el remitente del mensaje a la carga, agregar una propiedad al objeto:
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("ReceiveMessage", new
{
Sender = Context.User.Identity.Name,
Message = message
});
}

Los clientes más antiguos no se espera el Sender valor, por lo que podrá omitirlo. Un nuevo cliente puede
aceptarla mediante la actualización para leer la nueva propiedad:

connection.on("ReceiveMessage", (req) => {


let message = req.message;
if (req.sender) {
message = req.sender + ": " + message;
}
appendMessageToChatWindow(message);
});

En este caso, el nuevo cliente también es tolerante a errores de un servidor antiguo que no proporciona la Sender
valor. Dado que el servidor anterior no proporciona la Sender valor, el cliente comprueba si existe antes de acceder
a él.
Cliente de .NET de ASP.NET Core SignalR
18/06/2019 • 13 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.
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


Volver a conectar automáticamente
El HubConnection puede configurarse para volver a conectar automáticamente con el WithAutomaticReconnect
método en el HubConnectionBuilder. No conectar automáticamente de forma predeterminada.

HubConnection connection= new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chatHub"))
.WithAutomaticReconnect()
.Build();

Sin parámetros, WithAutomaticReconnect() configura el cliente que se debe esperar 0, 2, 10 y 30 segundos


respectivamente antes de intentar cada intento de volver a conectar, deteniéndose después de cuatro intentos con
error.
Antes de iniciar cualquier intento de volver a conectar el HubConnection le transición a la
HubConnectionState.Reconnecting de estado y se activan los Reconnecting eventos. Esto proporciona una
oportunidad para advertir a los usuarios que se ha perdido la conexión y deshabilitar los elementos de interfaz de
usuario. Puesta en cola o eliminación de mensajes, pueden iniciar aplicaciones no interactivas.

connection.Reconnecting += error =>


{
Debug.Assert(connection.State == HubConnectionState.Reconnecting);

// Notify users the connection was lost and the client is reconnecting.
// Start queuing or dropping messages.

return Task.CompletedTask;
};

Si el cliente vuelve a conectar correctamente dentro de sus primeros cuatro intentos, el HubConnection realizará la
transición a la Connected de estado y se activan los Reconnected eventos. Esto proporciona una oportunidad
para informar a los usuarios la conexión se ha restablecido y quitar de la cola los mensajes en cola.
Puesto que la conexión es completamente nuevo en el servidor, un nuevo ConnectionId se proporcionará el
Reconnected controladores de eventos.

WARNING
El Reconnected del controlador de eventos connectionId parámetro será nulo si el HubConnection se configuró para
omitir negociación.

connection.Reconnected += connectionId =>


{
Debug.Assert(connection.State == HubConnectionState.Connected);

// Notify users the connection was reestablished.


// Start dequeuing messages queued while reconnecting if any.

return Task.CompletedTask;
};

WithAutomaticReconnect() No configurar el HubConnection para volver a intentar errores de inicio inicial, por lo
que es necesario controlar manualmente los errores de inicio:
public static async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken token)
{
// Keep trying to until we can start or the token is canceled.
while (true)
{
try
{
await connection.StartAsync(token);
Debug.Assert(connection.State == HubConnectionState.Connected);
return true;
}
catch when (token.IsCancellationRequested)
{
return false;
}
catch
{
// Failed to connect, trying again in 5000 ms.
Debug.Assert(connection.State == HubConnectionState.Disconnected);
await Task.Delay(5000);
}
}
}

Si el cliente no volver a conectar correctamente dentro de sus primeros cuatro intentos, el HubConnection le
transición a la Disconnected de estado y se activan los Closed eventos. Esto proporciona una oportunidad para
intentar reiniciar la conexión manualmente o informar a los usuarios que la conexión se ha perdido
permanentemente.

connection.Closed += error =>


{
Debug.Assert(connection.State == HubConnectionState.Disconnected);

// Notify users the connection has been closed or manually try to restart the connection.

return Task.CompletedTask;
};

Con el fin de configurar un número personalizado de intentos de reconexión antes de desconectar o cambiar el
tiempo de reconexión, WithAutomaticReconnect acepta una matriz de números que representa el retraso en
milisegundos para esperar antes de iniciar cada intento de volver a conectar.

HubConnection connection= new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chatHub"))
.WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10) })
.Build();

// .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10),


TimeSpan.FromSeconds(30) }) yields the default behavior.

El ejemplo anterior se configura el HubConnection iniciar intentar reconexiones inmediatamente después de que
se pierde la conexión. Esto también es cierto para la configuración predeterminada.
Si se produce un error en el primer intento de volver a conectar, el segundo intento de reconexión también se
iniciará inmediatamente en lugar de esperar de 2 segundos como lo haría en la configuración predeterminada.
Si se produce un error en el segundo intento de volver a conectar, se iniciará al tercer intento de reconexión en 10
segundos, que es nuevo como la configuración predeterminada.
El comportamiento personalizado, a continuación, divergirá nuevo desde el comportamiento predeterminado
deteniendo tras la tercera conectarse de nuevo intento falla. En la configuración predeterminada se debía ser uno
más volver a conectarse intento en otros 30 segundos.
Si desea tener un mayor control sobre la planeación y el número de automático volver a conectarse, los intentos
de WithAutomaticReconnect acepta un objeto que implementa el IRetryPolicy interfaz, que tiene un solo método
denominado NextRetryDelay .
NextRetryDelay toma un único argumento con el tipo RetryContext . El RetryContext tiene tres propiedades:
PreviousRetryCount , ElapsedTime y RetryReason que son un long , un TimeSpan y un Exception
respectivamente. Antes del primer intento de reconexión ambos PreviousRetryCount y ElapsedTime será igual
cero y el RetryReason será la excepción que produjo la conexión se perderán. Después de cada intento de
reintento con errores, PreviousRetryCount se incrementa en uno, ElapsedTime se actualizará para reflejar la
cantidad de tiempo empleado en volver a conectarse hasta ahora y el RetryReason será la excepción que produjo
el último intento de volver a conectar a un error.
NextRetryDelay debe devolver ya sea un objeto TimeSpan que representa la hora debe esperar antes del
siguiente intento de volver a conectar o null si el HubConnection debe detenerse la reconexión.

public class RandomRetryPolicy : IRetryPolicy


{
private readonly Random _random = new Random();

public TimeSpan? NextRetryDelay(RetryContext retryContext)


{
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
if (retryContext.ElapsedTime < TimeSpan.FromSeconds(60))
{
return TimeSpan.FromSeconds(_random.Next() * 10);
}
else
{
// If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
return null;
}
}
}

HubConnection connection = new HubConnectionBuilder()


.WithUrl(new Uri("http://127.0.0.1:5000/chatHub"))
.WithAutomaticReconnect(new RandomRetryPolicy())
.Build();

Como alternativa, puede escribir código que se volverá a conectar el cliente manualmente como se muestra en
conectar manualmente.
Volver a conectar manualmente

WARNING
Antes de 3.0, el cliente de .NET para SignalR no volver a conectar automáticamente. Debe escribir código que se volverá a
conectar al cliente manualmente.

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

El InvokeAsync método devuelve un Task que se completa cuando finaliza el método de servidor. El valor
devuelto, si existe, se proporciona como el resultado de la Task . Las excepciones producidas por el método en el
servidor generan un error Task . Use await sintaxis que espere a que el método de servidor completar y
try...catch sintaxis para controlar los errores.

El SendAsync método devuelve un Task que se completa cuando el mensaje se envió al servidor. Se proporciona
ningún valor devuelto, ya que Task no espere hasta que finalice el método de servidor. Las excepciones
producidas en el cliente al enviar el mensaje generan un error Task . Use await y try...catch sintaxis para
controlar errores de envío.

NOTE
Si usa Azure SignalR Service en modo sin servidor, no puede llamar a métodos de concentrador desde un cliente. Para
obtener más información, consulte el documentación de SignalR Service.

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
Documentación sin servidor de Azure SignalR Service
Cliente ASP.NET Core SignalR Java
28/06/2019 • 6 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 1.0.0 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.signalr:signalr:1.0.0'

Si con Maven, agregue las siguientes líneas dentro de la <dependencies> elemento de su pom.xml archivo:

<dependency>
<groupId>com.microsoft.signalr</groupId>
<artifactId>signalr</artifactId>
<version>1.0.0</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 = HubConnectionBuilder.create(input)


.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);
NOTE
Si usa Azure SignalR Service en modo sin servidor, no puede llamar a métodos de concentrador desde un cliente. Para
obtener más información, consulte el documentación de SignalR Service.

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

Agregue un registro
El cliente de SignalR Java usa el SLF4J biblioteca para el registro. Es una API de alto nivel de registro que permite
a los usuarios de la biblioteca elegir su propia implementación de registro específico al incorporar una dependencia
de registro específico. El fragmento de código siguiente muestra cómo usar java.util.logging con el cliente de
SignalR Java.

implementation 'org.slf4j:slf4j-jdk14:1.7.25'

Si no configura el registro de las dependencias, SLF4J carga un registrador de ninguna operación de forma
predeterminada con el mensaje de advertencia siguiente:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".


SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Esto puede pasar por alto.

Notas de desarrollo de Android


Con respecto a la compatibilidad con el SDK de Android para las características de cliente de SignalR, tenga en
cuenta los siguientes elementos cuando se especifica la versión del SDK de Android de destino:
El cliente de Java de SignalR se ejecutará en el nivel de API Android 16 y versiones posteriores.
Conexión a través de Azure SignalR Service requerirá el nivel de API de Android 20 y versiones posterior
porque la Azure SignalR Service requiere TLS 1.2 y no es compatible con conjuntos de cifrado basado en SHA-
1. Android agrega compatibilidad con SHA-256 (y versiones posteriores) conjuntos de cifrado en el nivel de API
20.

Configurar la autenticación de token de portador


En el cliente de SignalR Java, puede configurar un token de portador a usar para la autenticación mediante un
"generador de tokens de acceso" para el HttpHubConnectionBuilder. Use withAccessTokenFactory para
proporcionar una RxJava único<cadena >. Con una llamada a Single.defer, puede escribir la lógica para generar
tokens de acceso para el cliente.
HubConnection hubConnection = HubConnectionBuilder.create("YOUR HUB URL HERE")
.withAccessTokenProvider(Single.defer(() -> {
// Your logic here.
return Single.just("An Access Token");
})).build();

Limitaciones conocidas
Se admite solo el protocolo JSON.
No se admiten el transporte de reserva y el transporte de eventos enviados del servidor.
Se admite solo el protocolo JSON.
Se admite sólo el transporte de WebSockets.
Transmisión por secuencias no se admite todavía.

Recursos adicionales
Referencia de API de Java
Usar concentradores en ASP.NET Core SignalR
Cliente ASP.NET Core SignalR JavaScript
Publicar un ASP.NET Core SignalR app en Azure App Service
Documentación sin servidor de Azure SignalR Service
Cliente ASP.NET Core SignalR JavaScript
20/06/2019 • 15 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().then(function () {
console.log("connected");
});

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 . El código de ejemplo usa la sintaxis de la función de flecha que se admiten en las
versiones actuales de todos los exploradores principales, excepto Internet Explorer.

connection.invoke("SendMessage", user, message).catch(err => console.error(err.toString()));

NOTE
Si usa Azure SignalR Service en modo sin servidor, no puede llamar a métodos de concentrador desde un cliente. Para
obtener más información, consulte el documentación de SignalR Service.

El invoke método devuelve un JavaScript promesa. El Promise se resuelve con el valor devuelto (si existe)
cuando se devuelve el método en el servidor. Si el método en el servidor produce un error, el Promise se
rechaza el mensaje de error. Use la then y catch métodos en el Promise para controlar estos casos (o await
sintaxis).
El send método devuelve un JavaScript Promise . El Promise se resuelve cuando se envió el mensaje al
servidor. Si se produce un error al enviar el mensaje, el Promise se rechaza el mensaje de error. Use la then y
catch métodos en el Promise para controlar estos casos (o await sintaxis).

NOTE
Uso de send no espera a que el servidor ha recibido el mensaje. Por lo tanto, no es posible devolver datos o errores del
servidor.

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(function (err) {
return 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();

Volver a conectar los clientes


Volver a conectar automáticamente
Se puede configurar el cliente de JavaScript de SignalR para volver a conectar automáticamente con el
withAutomaticReconnect método HubConnectionBuilder. No conectar automáticamente de forma
predeterminada.
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect()
.build();

Sin parámetros, withAutomaticReconnect() configura el cliente que se debe esperar 0, 2, 10 y 30 segundos


respectivamente antes de intentar cada intento de volver a conectar, deteniéndose después de cuatro intentos
con error.
Antes de iniciar cualquier intento de volver a conectar el HubConnection le transición a la
HubConnectionState.Reconnecting de estado y se activan su onreconnecting devoluciones de llamada en lugar de
realizar la transición a la Disconnected estado y desencadenar su onclose devoluciones de llamada como un
HubConnection sin volver a conectar automático configurado. Esto proporciona una oportunidad para advertir a
los usuarios que se ha perdido la conexión y deshabilitar los elementos de interfaz de usuario.

connection.onreconnecting((error) => {
console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

document.getElementById("messageInput").disabled = true;

const li = document.createElement("li");
li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
document.getElementById("messagesList").appendChild(li);
});

Si el cliente vuelve a conectar correctamente dentro de sus primeros cuatro intentos, el HubConnection realizará
la transición a la Connected de estado y activar su onreconnected devoluciones de llamada. Esto proporciona
una oportunidad para informar a los usuarios que se ha restablecido la conexión.
Puesto que la conexión es completamente nuevo en el servidor, un nuevo connectionId se proporcionará el
onreconnected devolución de llamada.

WARNING
El onreconnected la devolución de llamada connectionId parámetro quedará sin definir si la HubConnection se
configuró para omitir negociación.

connection.onreconnected((connectionId) => {
console.assert(connection.state === signalR.HubConnectionState.Connected);

document.getElementById("messageInput").disabled = false;

const li = document.createElement("li");
li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
document.getElementById("messagesList").appendChild(li);
});

withAutomaticReconnect() No configurar el HubConnection para volver a intentar errores de inicio inicial, por lo
que es necesario controlar manualmente los errores de inicio:
async function start() {
try {
await connection.start();
console.assert(connection.state === signalR.HubConnectionState.Connected);
console.log("connected");
} catch (err) {
console.assert(connection.state === signalR.HubConnectionState.Disconnected);
console.log(err);
setTimeout(() => start(), 5000);
}
};

Si el cliente no volver a conectar correctamente dentro de sus primeros cuatro intentos, el HubConnection le
transición a la Disconnected de estado y activar su onclose devoluciones de llamada. Esto proporciona una
oportunidad para informar a los usuarios la conexión ha perdido permanentemente y recomienda actualizar la
página:

connection.onclose((error) => {
console.assert(connection.state === signalR.HubConnectionState.Disconnected);

document.getElementById("messageInput").disabled = true;

const li = document.createElement("li");
li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the
connection.`;
document.getElementById("messagesList").appendChild(li);
});

Con el fin de configurar un número personalizado de intentos de reconexión antes de desconectar o cambiar el
tiempo de reconexión, withAutomaticReconnect acepta una matriz de números que representa el retraso en
milisegundos para esperar antes de iniciar cada intento de volver a conectar.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.withAutomaticReconnect([0, 0, 10000])
.build();

// .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior

El ejemplo anterior se configura el HubConnection iniciar intentar reconexiones inmediatamente después de que
se pierde la conexión. Esto también es cierto para la configuración predeterminada.
Si se produce un error en el primer intento de volver a conectar, el segundo intento de reconexión también se
iniciará inmediatamente en lugar de esperar de 2 segundos como lo haría en la configuración predeterminada.
Si se produce un error en el segundo intento de volver a conectar, se iniciará al tercer intento de reconexión en
10 segundos, que es nuevo como la configuración predeterminada.
El comportamiento personalizado, a continuación, divergirá nuevo del comportamiento predeterminado
deteniendo tras la reconexión tercer intento error en lugar de intentar una reconexión más intento en otros 30
segundos como lo haría en la configuración predeterminada.
Si desea tener un mayor control sobre la planeación y el número de automático volver a conectarse, los intentos
de withAutomaticReconnect acepta un objeto que implementa el IReconnectPolicy interfaz, que tiene un solo
método denominado nextRetryDelayInMilliseconds .
nextRetryDelayInMillisecondstoma dos argumentos, previousRetryCount y elapsedMilliseconds , que son
ambos números. Antes del primer intento de reconexión ambos previousRetryCount y elapsedMilliseconds
será igual cero. Después de cada intento de reintento con errores, previousRetryCount aumentará en uno y
elapsedMilliseconds se actualizará para reflejar la cantidad de tiempo empleado en volver a conectarse hasta
ahora en milisegundos.
nextRetryDelayInMilliseconds debe devolver un número que representa el número de milisegundos para
esperar antes del siguiente intento de volver a conectar o null si el HubConnection debe detenerse la
reconexión.

const connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (previousRetryCount, elapsedMilliseconds) => {
if (elapsedMilliseconds < 60000) {
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
return Math.random() * 10000;
} else {
// If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
return null;
}
}
})
.build();

Como alternativa, puede escribir código que se volverá a conectar el cliente manualmente como se muestra en
conectar manualmente.
Volver a conectar manualmente

WARNING
Antes de 3.0, el cliente de JavaScript de SignalR no volver a conectar automáticamente. Debe escribir código que se
volverá a conectar al cliente manualmente.

El código siguiente muestra un enfoque típico reconexión manual:


1. Una función (en este caso, el start función) se crea para iniciar la conexión.
2. Llame a la start función en la conexión onclose controlador de eventos.

async function start() {


try {
await connection.start();
console.log("connected");
} catch (err) {
console.log(err);
setTimeout(() => start(), 5000);
}
};

connection.onclose(async () => {
await start();
});

Una implementación real podría usar un retroceso exponencial o vuelva a intentar un número especificado de
veces antes de desistir.

Recursos adicionales
Referencia de API de JavaScript
Tutorial de JavaScript
Tutorial de TypeScript y WebPack
Concentradores
Cliente .NET
Publicar en Azure
Solicitudes entre orígenes (CORS )
Documentación sin servidor de Azure SignalR Service
Hospedaje de ASP.NET Core SignalR y escalado
10/05/2019 • 8 minutes to read • Edit Online

Por Andrew Stanton-Nurse, Brady Gaster, y Tom Dykstra,


En este artículo se explica de hospedaje y la escala para aplicaciones de alto tráfico que usan ASP.NET Core
SignalR.

Recursos de la conexión TCP


Se limita el número de conexiones TCP simultáneas que puede admitir un servidor web. Usan los clientes HTTP
estándares efímero conexiones. Cuando el cliente pasa a estado inactivo y se vuelve a abrir más adelante, se
pueden cerrar estas conexiones. Por otro lado, es una conexión SignalR persistente. Las conexiones de SignalR
permanecen abierta, incluso cuando el cliente pasa a estado inactivo. En una aplicación con mucho tráfico que
sirve a muchos clientes, estas conexiones persistentes pueden provocar que los servidores alcance su número
máximo de conexiones.
Las conexiones persistentes también consumen memoria adicional, para realizar un seguimiento de cada conexión.
Un uso intensivo de recursos relacionados con la conexión signalr puede afectar a otras aplicaciones web que se
hospedan en el mismo servidor. Cuando SignalR se abre y contiene el último conexiones TCP disponibles, otras
aplicaciones web en el mismo servidor también tienen no hay más conexiones disponibles para ellos.
Si un servidor se ejecuta fuera de las conexiones, verá errores aleatorios de socket y errores de restablecimiento de
conexión. Por ejemplo:

An attempt was made to access a socket in a way forbidden by its access permissions...

Para evitar el uso de recursos de SignalR causando errores en otras aplicaciones web, ejecute SignalR en distintos
servidores de las otras aplicaciones web.
Para evitar el uso de recursos de SignalR causando errores en una aplicación de SignalR, escale horizontalmente
para limitar el número de conexiones de que un servidor tiene que administrar.

Escalar
Una aplicación que usa SignalR necesita realizar un seguimiento de todas sus conexiones, lo que crea problemas
de una granja de servidores. Agregar un servidor y obtiene nuevas conexiones que no conocen los demás
servidores. Por ejemplo, no es consciente de las conexiones de los demás servidores SignalR en cada servidor en
el diagrama siguiente. Cuando desea SignalR en uno de los servidores enviar un mensaje a todos los clientes, el
mensaje sólo se dirige a los clientes conectados a ese servidor.
Las opciones para solucionar este problema son las Azure SignalR Service y Redis backplane.

Servicio Azure SignalR


Azure SignalR Service es un servidor proxy en lugar de como un backplane. Cada vez que un cliente inicia una
conexión al servidor, se redirige el cliente para conectarse al servicio. Este proceso se ilustra en el diagrama
siguiente:

El resultado es que el servicio administra todas las conexiones de cliente, mientras que cada servidor necesita solo
un pequeño número constante de las conexiones al servicio, tal como se muestra en el diagrama siguiente:

Este enfoque de escalado tiene varias ventajas respecto a la alternativa de backplane de Redis:
Sesiones permanentes, también conocidas como afinidad del cliente, no es necesario, porque los clientes se
redirigen inmediatamente a Azure SignalR Service cuando se conectan.
Una aplicación puede escalar horizontalmente de SignalR en función del número de mensajes enviados,
mientras que el servicio Azure SignalR se escala automáticamente para administrar cualquier número de
conexiones. Por ejemplo, podría haber miles de clientes, pero si solo se envían algunos mensajes por segundo,
la aplicación de SignalR no tendrá que escalar horizontalmente a varios servidores para controlar las
conexiones a sí mismos.
Una aplicación de SignalR que no utilice muchos más recursos de conexión que una aplicación web sin SignalR.
Por estas razones, se recomienda Azure SignalR Service para todas las aplicaciones de ASP.NET Core SignalR
hospedadas en Azure, como App Service, las máquinas virtuales y contenedores.
Para obtener más información, consulte el documentación de Azure SignalR Service.

Backplane de Redis
Redis es un almacén de pares clave-valor en memoria que admite un sistema de mensajería con un modelo de
publicación/suscripción. El backplane SignalR Redis usa la característica de pub/sub para reenviar los mensajes a
otros servidores. Cuando un cliente realiza una conexión, la información de conexión se pasa al backplane. Cuando
un servidor desea enviar un mensaje a todos los clientes, envía al backplane. El backplane sabe conectados todos
los clientes y que los servidores que están incluidas en. Envía el mensaje a todos los clientes a través de sus
respectivos servidores. Este proceso se ilustra en el diagrama siguiente:

El backplane de Redis es el enfoque recomendado de escalabilidad horizontal para aplicaciones hospedadas en su


propia infraestructura. Azure SignalR Service no es una opción práctica para su uso en producción con
aplicaciones locales debido a la latencia de la conexión entre su centro de datos y un centro de datos de Azure.
Las ventajas de Azure SignalR Service se ha indicado anteriormente son las desventajas de la placa de Redis:
Sesiones permanentes, también conocidas como afinidad del cliente, es necesario. Una vez que se inicia una
conexión en un servidor, la conexión debe permanecer en ese servidor.
Una aplicación de SignalR debe escalar horizontalmente basada en el número de clientes, incluso si algunos de
los mensajes se envíen.
Una aplicación de SignalR usa significativamente más recursos de conexión que una aplicación web sin
SignalR.

Pasos siguientes
Para obtener más información, vea los siguientes recursos:
Documentación de Azure SignalR Service
Configurar un backplane de Redis
Publicar un ASP.NET Core SignalR app en Azure
App Service
27/06/2019 • 5 minutes to read • Edit Online

Por Brady Gaster


Azure App Service 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. Para obtener más
información, consulte SignalR service para Azure.

Publicar la aplicación
Este artículo trata la publicación con las herramientas de Visual Studio. Pueden usar los usuarios de Visual
Studio Code CLI de Azure comandos para publicar aplicaciones en Azure. Para obtener más información,
consulte publicar una aplicación ASP.NET Core en Azure con herramientas de línea de comandos.
1. Desde el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione
Publicar.
2. Confirme que App Service y crear nuevo están seleccionados en el elegir un destino de publicación
cuadro de diálogo.
3. Seleccione Crear perfil desde el publicar botón colocar hacia abajo.
Escriba la información descrita en la siguiente tabla en la crear App Service cuadro de diálogo y
seleccione crear.

ELEMENTO DESCRIPCIÓN

Name Nombre único de la aplicación.

Suscripción Suscripción de Azure que usa la aplicación.

Grupo de recursos Grupo de recursos relacionados a la que pertenece la


aplicación.

Plan de hospedaje Plan de precios para la aplicación web.

4. Seleccione el Azure SignalR Service en el dependencias > agregar lista desplegable:

5. En el Azure SignalR Service cuadro de diálogo, seleccione crear una nueva instancia de Azure
SignalR Service.
6. Proporcione un nombre, grupo de recursos, y ubicación. Vuelva a la Azure SignalR Service cuadro
de diálogo y seleccione agregar.
Visual Studio realiza las tareas siguientes:
Crea un perfil de publicación que contiene la configuración de publicación.
Crea un Azure Web App con los detalles proporcionados.
Publica la aplicación.
Inicia un explorador, que carga la aplicación web.
El formato de dirección URL de la aplicación es {APP SERVICE NAME}.azurewebsites.net . Por ejemplo, una
aplicación denominada SignalRChatApp tiene una dirección URL de https://signalrchatapp.azurewebsites.net .
Si un HTTP 502.2 - puerta de enlace incorrecta se produce un error al implementar una aplicación que tenga
como destino una versión de .NET Core de versión preliminar, consulte versión de vista previa de la
implementación de ASP.NET Core en Azure App Service para resolverlo.

Configuración de la aplicación en Azure App Service


NOTE
En esta sección solo se aplica a las aplicaciones no usan Azure SignalR Service.
Si la aplicación usa Azure SignalR Service, el servicio de aplicación no requiere la configuración de afinidad de enrutamiento
de solicitud de aplicaciones (ARR) y Web Sockets descritos en esta sección. Los clientes conectan los Sockets Web a Azure
SignalR Service, no directamente a la aplicación.

Para las aplicaciones hospedadas sin Azure SignalR Service, habilitar:


Afinidad ARR para enrutar las solicitudes de un usuario a la misma instancia de App Service. El valor
predeterminado es en.
Web Sockets para permitir que el transporte de Web Sockets para la función. El valor predeterminado es
desactivar.
1. En el portal de Azure, vaya a la aplicación web en App Services.
2. Abra configuración > configuración General.
3. Establecer sockets Web a en.
4. Compruebe que afinidad ARR está establecido en en.

Limita el Plan de App Service


Web Sockets y otros transportes están limitados según el Plan de App Service seleccionado. Para obtener más
información, consulte el límites de Azure Cloud Services y límites de App Service secciones de la suscripción de
Azure y límites de servicio, cuotas y restricciones artículo.

Recursos adicionales
¿Qué es Azure SignalR Service?
Introducción a ASP.NET Core SignalR
Hospedaje e implementación de ASP.NET Core
Publicar una aplicación de ASP.NET Core en Azure con Visual Studio
Publicar una aplicación ASP.NET Core en Azure con herramientas de línea de comandos
Hospedar e implementar aplicaciones de la versión preliminar de ASP.NET Core en Azure
Configurar un backplane de Redis de escalado
horizontal de SignalR de ASP.NET Core
18/06/2019 • 7 minutes to read • Edit Online

Por Andrew Stanton-Nurse, Brady Gaster, y Tom Dykstra,


En este artículo se explica los aspectos de SignalR específicos de la configuración de un Redis servidor que se
usará para el escalado horizontal de una aplicación ASP.NET Core SignalR.

Configurar un backplane de Redis


Implementar un servidor de Redis.

IMPORTANT
Para su uso en producción, se recomienda un backplane de Redis solo cuando se ejecuta en el mismo centro de datos
que la aplicación de SignalR. En caso contrario, latencia de red disminuye el rendimiento. Si se ejecuta la aplicación de
SignalR en la nube de Azure, se recomienda Azure SignalR Service en lugar de un backplane de Redis. Puede usar el
servicio Azure Redis Cache para el desarrollo y entornos de prueba.

Para obtener más información, vea los siguientes recursos:


Hospedaje de producción de ASP.NET Core SignalR y escalado
Documentación de Redis
Documentación de Azure Redis Cache
En la aplicación de SignalR, instale el Microsoft.AspNetCore.SignalR.Redis paquete NuGet. (También hay un
Microsoft.AspNetCore.SignalR.StackExchangeRedis empaquetar, pero que uno es para ASP.NET Core 2.2 y
versiones posteriores.)
En el Startup.ConfigureServices método, llame a AddRedis después AddSignalR :

services.AddSignalR().AddRedis("<your_Redis_connection_string>");

Configure las opciones según sea necesario:


Se puede establecer la mayoría de las opciones en la cadena de conexión o en el ConfigurationOptions
objeto. Las opciones especificadas en ConfigurationOptions invalidará la establece en la cadena de
conexión.
El ejemplo siguiente muestra cómo establecer las opciones el ConfigurationOptions objeto. Este ejemplo
agrega un prefijo de canal para que varias aplicaciones pueden compartir la misma instancia de Redis,
como se explica en el paso siguiente.

services.AddSignalR()
.AddRedis(connectionString, options => {
options.Configuration.ChannelPrefix = "MyApp";
});

En el código anterior, options.Configuration se inicializa con todo lo que se especificó en la cadena de


conexión.
En la aplicación de SignalR, instale uno de los siguientes paquetes NuGet:
Microsoft.AspNetCore.SignalR.StackExchangeRedis-Depende de StackExchange.Redis 2.X.X. Este es el
paquete recomendado para ASP.NET Core 2.2 y versiones posteriores.
Microsoft.AspNetCore.SignalR.Redis -Depende 1.X.X StackExchange.Redis. Este paquete no estará
disponible en ASP.NET Core 3.0.
En el Startup.ConfigureServices método, llame a AddStackExchangeRedis después AddSignalR :

services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");

Configure las opciones según sea necesario:


Se puede establecer la mayoría de las opciones en la cadena de conexión o en el ConfigurationOptions
objeto. Las opciones especificadas en ConfigurationOptions invalidará la establece en la cadena de
conexión.
El ejemplo siguiente muestra cómo establecer las opciones el ConfigurationOptions objeto. Este ejemplo
agrega un prefijo de canal para que varias aplicaciones pueden compartir la misma instancia de Redis,
como se explica en el paso siguiente.

services.AddSignalR()
.AddStackExchangeRedis(connectionString, options => {
options.Configuration.ChannelPrefix = "MyApp";
});

En el código anterior, options.Configuration se inicializa con todo lo que se especificó en la cadena de


conexión.
Para obtener información acerca de las opciones de Redis, consulte el documentación Redis StackExchange.
Si usa un servidor de Redis para varias aplicaciones de SignalR, use un prefijo de canal diferentes para cada
aplicación de SignalR.
Establecer un prefijo de canal aísla una aplicación de SignalR de otras personas que utilizan los prefijos de
canal diferente. Si no asigna prefijos diferentes, un mensaje enviado desde una aplicación a todos sus
propios clientes irá a todos los clientes de todas las aplicaciones que usan el servidor de Redis como
backplane.
Configure su servidor granja equilibrio de carga software para sesiones permanentes. Estos son algunos
ejemplos de documentación sobre cómo hacerlo:
IIS
HAProxy
Nginx
pfSense

Errores del servidor de Redis


Cuando un servidor de Redis deja de funcionar, SignalR genera excepciones que indican que no se entregarán los
mensajes. Algunos mensajes de excepción típico:
Mensaje de error de escritura
No se pudo invocar el método de concentrador 'NombreMétodo'
Error de conexión con Redis
SignalR no almacena en búfer los mensajes que les envíe cuando el servidor vuelve a activarse. Se pierden los
mensajes enviados mientras el servidor Redis está inactivo.
SignalR vuelve a conectarse automáticamente cuando el servidor de Redis esté disponible de nuevo.
Comportamiento personalizado para los errores de conexión
Este es un ejemplo que muestra cómo controlar eventos de error de conexión de Redis.

services.AddSignalR()
.AddRedis(o =>
{
o.ConnectionFactory = async writer =>
{
var config = new ConfigurationOptions
{
AbortOnConnectFail = false
};
config.EndPoints.Add(IPAddress.Loopback, 0);
config.SetDefaultPorts();
var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
connection.ConnectionFailed += (_, e) =>
{
Console.WriteLine("Connection to Redis failed.");
};

if (!connection.IsConnected)
{
Console.WriteLine("Did not connect to Redis.");
}

return connection;
};
});

services.AddSignalR()
.AddMessagePackProtocol()
.AddStackExchangeRedis(o =>
{
o.ConnectionFactory = async writer =>
{
var config = new ConfigurationOptions
{
AbortOnConnectFail = false
};
config.EndPoints.Add(IPAddress.Loopback, 0);
config.SetDefaultPorts();
var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
connection.ConnectionFailed += (_, e) =>
{
Console.WriteLine("Connection to Redis failed.");
};

if (!connection.IsConnected)
{
Console.WriteLine("Did not connect to Redis.");
}

return connection;
};
});
Agrupación en clústeres de Redis
Agrupación en clústeres de Redis es un método para lograr una alta disponibilidad mediante el uso de varios
servidores de Redis. Oficialmente no se admite la agrupación en clústeres, pero es posible que funcione.

Pasos siguientes
Para obtener más información, vea los siguientes recursos:
Hospedaje de producción de ASP.NET Core SignalR y escalado
Documentación de Redis
Documentación de Redis StackExchange
Documentación de Azure Redis Cache
Host ASP.NET Core SignalR en servicios en segundo
plano
10/05/2019 • 5 minutes to read • Edit Online

Por Brady Gaster


En este artículo se proporciona instrucciones para:
Hospedaje de concentradores de SignalR con un proceso de trabajo en segundo plano hospedado con
ASP.NET Core.
Envío de mensajes a los clientes desde dentro de un núcleo de .NET conectados BackgroundService.
Ver o descargar el código de ejemplo (cómo descargar)

Conectar SignalR durante el inicio


Hospedaje de concentradores de SignalR de ASP.NET Core en el contexto de un proceso de trabajo en segundo
plano es idéntico al hospedaje de Hub en una aplicación web ASP.NET Core. En el Startup.ConfigureServices , la
llamada a método services.AddSignalR agrega los servicios necesarios a la capa de inserción de dependencias de
ASP.NET Core (DI) para admitir SignalR. En Startup.Configure , el UseSignalR se llama al método para conectar
los puntos de conexión de concentrador en la canalización de solicitudes de ASP.NET Core.

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddHostedService<Worker>();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseSignalR((routes) =>
{
routes.MapHub<ClockHub>("/hubs/clock");
});
}
}

En el ejemplo anterior, el ClockHub la clase implementa la Hub<T> clase para crear un concentrador fuertemente
tipado. El ClockHub se ha configurado en el Startup clase responder a las solicitudes en el punto de conexión
/hubs/clock .

Para obtener más información sobre los concentradores fuertemente tipados, vea usar concentradores de SignalR
para ASP.NET Core.
NOTE
Esta funcionalidad no se limita a la concentrador<T > clase. Cualquier clase que hereda de concentrador, tales como
DynamicHub, también funcionará.

public class ClockHub : Hub<IClock>


{
public async Task SendTimeToClients(DateTime dateTime)
{
await Clients.All.ShowTime(dateTime);
}
}

La interfaz utilizada por fuertemente tipado ClockHub es el IClock interfaz.

public interface IClock


{
Task ShowTime(DateTime currentTime);
}

Llamar a un concentrador SignalR desde un servicio en segundo plano


Durante el inicio, el Worker (clase), un BackgroundService , está dispuesta utilizando AddHostedService .

services.AddHostedService<Worker>();

Puesto que también está dispuesta SignalR durante el Startup fase, en que cada concentrador se conecta a un
punto de conexión individual en la canalización de solicitudes HTTP de ASP.NET Core, cada concentrador se
representa mediante un IHubContext<T> en el servidor. Características de inserción de dependencias de mediante
ASP.NET Core, otras clases que se crea una instancia de la capa de hospedaje, como BackgroundService clases, las
clases de controlador de MVC o modelos de página de Razor, pueden obtener las referencias a los concentradores
de servidor mediante la aceptación de las instancias de IHubContext<ClockHub, IClock> durante la construcción.

public class Worker : BackgroundService


{
private readonly ILogger<Worker> _logger;
private readonly IHubContext<ClockHub, IClock> _clockHub;

public Worker(ILogger<Worker> logger, IHubContext<ClockHub, IClock> clockHub)


{
_logger = logger;
_clockHub = clockHub;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)


{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation($"Worker running at: {DateTime.Now}");
await _clockHub.Clients.All.ShowTime(DateTime.Now);
await Task.Delay(1000);
}
}
}

Como el ExecuteAsync método se llama de forma iterativa en el servicio en segundo plano, el servidor de la fecha
y hora actuales se envían a los clientes conectados mediante el ClockHub .

Reacción a eventos de SignalR con servicios en segundo plano


Al igual que una aplicación de página única mediante el cliente de JavaScript para SignalR o una aplicación de
escritorio de .NET puede hacer mediante el uso de la Cliente de .NET de ASP.NET Core SignalR, un
BackgroundService o IHostedService implementación también se puede usar para conectarse a los concentradores
de SignalR y responder a eventos.
El ClockHubClient clase implementa tanto la IClock interfaz y la IHostedService interfaz. De este modo puede
dispuesta durante Startup ejecutarse continuamente y responder a eventos de Hub desde el servidor.

public partial class ClockHubClient : IClock, IHostedService


{
}

Durante la inicialización, el ClockHubClient crea una instancia de un HubConnection y conecta la IClock.ShowTime


método como controlador para el concentrador ShowTime eventos.

private readonly ILogger<ClockHubClient> _logger;


private HubConnection _connection;

public ClockHubClient(ILogger<ClockHubClient> logger)


{
_logger = logger;

_connection = new HubConnectionBuilder()


.WithUrl(Strings.HubUrl)
.Build();

_connection.On<DateTime>(Strings.Events.TimeSent,
dateTime => _ = ShowTime(dateTime));
}

public Task ShowTime(DateTime currentTime)


{
_logger.LogInformation($"{currentTime.ToShortTimeString()}");

return Task.CompletedTask;
}

En el IHostedService.StartAsync implementación, el HubConnection se inicia de forma asincrónica.

public async Task StartAsync(CancellationToken cancellationToken)


{
// Loop is here to wait until the server is running
while (true)
{
try
{
await _connection.StartAsync(cancellationToken);

break;
}
catch
{
await Task.Delay(1000);
}
}
}
Durante la IHostedService.StopAsync método, el HubConnection se desecha de forma asincrónica.

public Task StopAsync(CancellationToken cancellationToken)


{
return _connection.DisposeAsync();
}
}

Recursos adicionales
Introducción
Concentradores
Publicar en Azure
Concentradores fuertemente tipados
Configuración de ASP.NET Core SignalR
02/07/2019 • 30 minutes to read • Edit Online

Opciones de serialización de JSON/MessagePack


SignalR de ASP.NET Core 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

ClientTimeoutInterval 30 segundos El servidor tendrá en cuenta el cliente


desconectado si no ha recibido un
mensaje (incluido keep-alive) en este
intervalo. Puede tardar más de este
intervalo de tiempo de espera para el
cliente realmente marcarse
desconectado debido a su
implementación. El valor recomendado
es doble el KeepAliveInterval valor.

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.
OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

StreamBufferCapacity 10 El número máximo de elementos que


pueden almacenarse en búfer para el
cliente cargue secuencias. Si se alcanza
este límite, el procesamiento de las
llamadas se bloquea hasta que el
servidor procesa los elementos de la
secuencia.

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

ClientTimeoutInterval 30 segundos El servidor tendrá en cuenta el cliente


desconectado si no ha recibido un
mensaje (incluido keep-alive) en este
intervalo. Puede tardar más de este
intervalo de tiempo de espera para el
cliente realmente marcarse
desconectado debido a su
implementación. El valor recomendado
es doble el KeepAliveInterval valor.

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.
OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

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.

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:

services.AddSignalR().AddHubOptions<MyHub>(options =>
{
options.EnableDetailedErrors = true;
});

Opciones de configuración de HTTP avanzadas


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 > en
Startup.Configure .

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseSignalR((configure) =>
{
var desiredTransports =
HttpTransportType.WebSockets |
HttpTransportType.LongPolling;

configure.MapHub<MyHub>("/myhub", (options) =>


{
options.Transports = desiredTransports;
});
});
}

En la tabla siguiente se describe las opciones para configurar las opciones avanzadas de HTTP de ASP.NET Core
SignalR:

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.
OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

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). También está disponible en el cliente de Java, pero la HttpHubConnectionBuilder subclase es lo que
contiene las opciones de configuración de generador, 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();

En lugar de un LogLevel valor, también puede proporcionar un string valor que representa un nombre de nivel
de registro. Esto es útil al configurar SignalR registro en entornos donde no tiene acceso a la LogLevel constantes.

let connection = new signalR.HubConnectionBuilder()


.withUrl("/myhub")
.configureLogging("warn")
.build();

En la tabla siguiente se enumera los niveles de registro disponibles. El valor que proporcione a configureLogging
establece la mínimo nivel que se registrarán de registro. Los mensajes registrados en este nivel, o los niveles se
muestran después de él en la tabla, se registrarán.

CADENA LOGLEVEL

"trace" LogLevel.Trace

"debug" LogLevel.Debug

"info" O "information" LogLevel.Information

"warn" O "warning" LogLevel.Warning

"error" LogLevel.Error
CADENA LOGLEVEL

"critical" LogLevel.Critical

"none" LogLevel.None

NOTE
Para deshabilitar el registro por completo, especifique signalR.LogLevel.None en el configureLogging método.

Para obtener más información sobre el registro, consulte el documentación de diagnóstico de SignalR.
El cliente de SignalR Java usa el SLF4J biblioteca para el registro. Es una API de alto nivel de registro que permite
a los usuarios de la biblioteca elegir su propia implementación de registro específico al incorporar una
dependencia de registro específico. El fragmento de código siguiente muestra cómo usar java.util.logging con el
cliente de SignalR Java.

implementation 'org.slf4j:slf4j-jdk14:1.7.25'

Si no configura el registro de las dependencias, SLF4J carga un registrador de ninguna operación de forma
predeterminada con el mensaje de advertencia siguiente:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".


SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Esto puede pasar por alto.


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

En esta versión de Java websockets de cliente es el transporte solo está disponible.


En el cliente de Java, el transporte está activado con la withTransport método en el HttpHubConnectionBuilder . De
forma predeterminada, el cliente de Java que usa el transporte de WebSockets.

HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/myhub")


.withTransport(TransportEnum.WEBSOCKETS)
.build();

NOTE
El cliente de SignalR Java todavía no admite transporte de reserva.

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

En el cliente de SignalR Java, puede configurar un token de portador a usar para la autenticación mediante un
generador de token de acceso a la HttpHubConnectionBuilder. Use withAccessTokenFactory para proporcionar
una RxJava único<cadena >. Con una llamada a Single.defer, puede escribir la lógica para generar tokens de
acceso para el cliente.

HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/myhub")


.withAccessTokenProvider(Single.defer(() -> {
// Your logic here.
return Single.just("An 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:
.NET
JavaScript
Java

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

ServerTimeout 30 segundos (30.000 milisegundos) Tiempo de espera para la 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.

HandshakeTimeout 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.
Configurar opciones adicionales
Se pueden configurar opciones adicionales en el WithUrl ( withUrl en JavaScript) método HubConnectionBuilder
o en las diversas API de configuración en el HttpHubConnectionBuilder en el cliente de Java:
.NET
JavaScript
Java

OPCIÓN DE .NET VALOR PREDETERMINADO DESCRIPCIÓN

AccessTokenProvider null Una función que devuelve una cadena


que se proporciona como un token de
autenticación de portador en solicitudes
HTTP.
OPCIÓN DE .NET VALOR PREDETERMINADO DESCRIPCIÓN

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 Empty Una colección de certificados TLS que se


envían autenticar las solicitudes.

Cookies Empty Una colección de cookies HTTP para


enviar con cada solicitud HTTP.

Credentials Empty Credenciales que se enviará con todas


las solicitudes HTTP.

CloseTimeout 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 Empty Una asignación de encabezados HTTP


adicionales que se enviará con todas las
solicitudes HTTP.

HttpMessageHandlerFactory 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 null Un proxy HTTP que se utilizará al enviar


solicitudes HTTP.

UseDefaultCredentials 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.
OPCIÓN DE .NET VALOR PREDETERMINADO DESCRIPCIÓN

WebSocketConfiguration null Un delegado que puede usarse para


configurar opciones adicionales de
WebSocket. Recibe una instancia de
ClientWebSocketOptions que puede
utilizarse para configurar las opciones.

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

En el cliente de Java, estas opciones pueden configurarse con los métodos en el HttpHubConnectionBuilder
devuelto desde el HubConnectionBuilder.create("HUB URL")

HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/myhub")


.withHeader("Foo", "Bar")
.shouldSkipNegotiate(true)
.withHandshakeResponseTimeout(30*1000)
.build();

Recursos adicionales
Introducción a SignalR de 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
10/05/2019 • 11 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.
El siguiente es un ejemplo de Startup.Configure que usa la autenticación de SignalR y ASP.NET Core:

public void Configure(IApplicationBuilder app)


{
...

app.UseStaticFiles();

app.UseAuthentication();

app.UseSignalR(hubs =>
{
hubs.MapHub<ChatHub>("/chat");
});

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

NOTE
Es importante el orden en que se registre el middleware de autenticación de SignalR y ASP.NET Core. Llame siempre a
UseAuthentication antes UseSignalR poder SignalR tiene un usuario en el HttpContext .

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.
Las cookies son una manera específica del explorador para enviar tokens de acceso, pero los clientes sin
explorador pueden enviarlos. 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
El cliente puede proporcionar un token de acceso en lugar de usar una cookie. El servidor valida el token y lo usa
para identificar al usuario. Esta validación se realiza solo cuando se establece la conexión. Durante la vida de la
conexión, el servidor no validar automáticamente para comprobar la revocación del token.
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>();

// Change to use email as the user identifier for SignalR


// services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the


// EmailBasedUserIdProvider, but do not use both.
}

Cookies frente a los tokens de portador


Dado que las cookies son específicas de los exploradores, enviarlos de otros tipos de clientes agrega complejidad
al comparado con el envío de tokens de portador. Por este motivo, la autenticación de cookies no se recomienda a
menos que la aplicación solo necesita autenticar a los usuarios desde el explorador del cliente. Autenticación de
token de portador es el enfoque recomendado al usar a los clientes que no sea el explorador del cliente.
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?.Identity?.Name;
}
}

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.

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.
Uso de notificaciones para personalizar el control de identidad
Una aplicación que autentica a los usuarios puede derivar los identificadores de usuario de SignalR de
notificaciones de usuario. Para especificar cómo SignalR crea los identificadores de usuario, implemente
IUserIdProvider y registre la implementación.

El código de ejemplo muestra cómo se podría usar notificaciones para seleccionar la dirección de correo
electrónico del usuario como la propiedad de identificación.

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.
public class EmailBasedUserIdProvider : IUserIdProvider
{
public virtual string GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
}
}

El registro de la cuenta agrega una notificación con el tipo ClaimsTypes.Email a la base de datos de identidad
ASP.NET.

// create a new user


var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user


await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

Registrar este componente en su Startup.ConfigureServices .

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

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

Recursos adicionales
Autenticación de Token de portador en ASP.NET Core
Consideraciones de seguridad en ASP.NET Core
SignalR
10/05/2019 • 9 minutes to read • Edit Online

Por Andrew Stanton-Nurse


En este artículo se proporciona información sobre la protección de SignalR.

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 un dominio distinto de la aplicación
de SignalR, middleware CORS debe habilitarse para permitir que el código JavaScript para conectarse a la
aplicación de SignalR. Permitir solicitudes entre orígenes solo desde el control o dominios que confía. Por ejemplo:
El sitio se hospeda en http://www.example.com
La aplicación de SignalR se hospeda en http://signalr.example.com
Debe configurarse la 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 solicitudes habilitar de origen cruzado (CORS ).
SignalR requiere las siguientes directivas CORS:
Permitir los orígenes específicos esperados. Permitir cualquier origen, es posible pero es no seguro o
recomendada.
Métodos HTTP GET y POST debe estar permitido.
Deben habilitarse las credenciales, incluso cuando no se usa la autenticación.
Por ejemplo, la siguiente directiva CORS permite hospedado en un cliente del explorador SignalR
https://example.com para tener acceso a la aplicación de SignalR hospedada en https://signalr.example.com :

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
// ... other middleware ...

// Make sure the CORS middleware is ahead of SignalR.


app.UseCors(builder =>
{
builder.WithOrigins("https://example.com")
.AllowAnyHeader()
.WithMethods("GET", "POST")
.AllowCredentials();
});

// ... other middleware ...

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

// ... other middleware ...


}
NOTE
SignalR no es compatible con la característica CORS integrada en Azure App Service.

Restricción de origen de WebSocket


Las protecciones proporcionadas por CORS no se aplican a WebSockets. Para la restricción de origen de
WebSockets, lea restricción de origen de WebSockets.
Las protecciones proporcionadas por CORS no se aplican a WebSockets. Los exploradores no hacen lo siguiente:
Efectúan solicitudes preparatorias CORS.
Respetan las restricciones especificadas en los encabezados Access-Control al efectuar solicitudes de
WebSocket.
En cambio, sí que envían el encabezado Origin al emitir solicitudes de WebSocket. Las aplicaciones deben
configurarse para validar estos encabezados a fin de garantizar que solo se permitan los WebSockets procedentes
de los orígenes esperados.
En ASP.NET Core 2.1 y versiones posteriores, la validación del encabezado puede lograrse mediante un
middleware personalizado colocarlo antes UseSignalR y el middleware de autenticación en Configure :
// In Startup, add a static field listing the allowed Origin values:
private static readonly HashSet<string> _allowedOrigins = new HashSet<string>()
{
// Add allowed origins here. For example:
"https://www.mysite.com",
"https://mysite.com",
};

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
// ... other middleware ...

// Validate Origin header on WebSocket requests to prevent unexpected cross-site


// WebSocket requests.
app.Use((context, next) =>
{
// Check for a WebSocket request.
if (string.Equals(context.Request.Headers["Upgrade"], "websocket"))
{
var origin = context.Request.Headers["Origin"];

// If there is an origin header, and the origin header doesn't match


// an allowed value:
if (!string.IsNullOrEmpty(origin) && !_allowedOrigins.Contains(origin))
{
// The origin is not allowed, reject the request
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
}

// The request is a valid Origin or not a WebSocket request, so continue.


return next();
});

// ... other middleware ...

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

// ... other middleware ...


}

NOTE
El encabezado Origin está controlado por el cliente y, al igual que el encabezado Referer , se puede falsificar. Estos
encabezados deben no utilizarse como mecanismo de autenticación.

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.
Recibir el token de acceso a través de la cadena de consulta es normalmente tan seguro como usar el estándar
Authorization encabezado. Siempre debe usar HTTPS para garantizar una conexión segura de extremo a otro
entre el cliente y el servidor. La dirección URL para cada solicitud, incluida la cadena de consulta de registro de
muchos servidores web. Las direcciones URL de registro, es posible que registre el token de acceso. ASP.NET
Core, se registra la dirección URL para cada solicitud de forma predeterminada, que incluirá la cadena de consulta.
Por ejemplo:
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:5000/myhub?access_token=1234

Si le preocupa sobre el registro de estos datos con los registros de servidor, puede deshabilitar este registro
mediante la configuración de la Microsoft.AspNetCore.Hosting registrador para el Warning nivel o superior (estos
mensajes se escriben en Info nivel). Consulte la documentación sobre filtrado del registro para obtener más
información. Si aún desea cierta información de solicitud de registro, puede escribir un middleware para registrar
los datos que necesita y filtra el access_token el valor de cadena de consulta (si existe).

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.Entrega de
mensajes de excepción al cliente se puede invalidar (por ejemplo, en el desarrollo o pruebas) con
EnableDetailedErrors . Los mensajes de excepción no se deben exponer al cliente en aplicaciones de producción.

Administración de búfer
SignalR usa búferes por conexión para administrar los mensajes entrantes y salientes. De forma predeterminada,
SignalR limita estos búferes a 32 KB. El mensaje más grande que puede enviar un cliente o servidor es 32 KB. La
memoria máxima utilizada por una conexión para los mensajes es 32 KB. Si los mensajes siempre son menores
que 32 KB, puede reducir el límite, que:
Impide que un cliente puede enviar un mensaje mayor.
El servidor nunca tendrá que asignar los búferes grandes para aceptar mensajes.
Si los mensajes son superiores a 32 KB, puede aumentar el límite. Aumentar este límite significa:
El cliente puede hacer que el servidor asignar búferes de memoria de gran tamaño.
Asignación de servidor de los búferes grandes puede reducir el número de conexiones simultáneas.
Hay límites para los 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 (incluidos los valores devueltos de métodos de concentrador) supere ese límite, se
producirá una excepción.
Establecer el límite en 0 desactivar el límite. Quitar el límite permite que un cliente enviar un mensaje de cualquier
tamaño. Los clientes malintencionados enviar mensajes de gran tamaño pueden producir un exceso de memoria
que se va a asignar. Uso de memoria excesivo puede reducir significativamente el número de conexiones
simultáneas.
Usar el protocolo de MessagePack concentrador de
SignalR para ASP.NET Core
10/05/2019 • 9 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


NOTE
JSON está habilitada de forma predeterminada para los clientes compatibles. Los clientes pueden admitir solo un único
protocolo. Agregar compatibilidad con MessagePack reemplazará cualquier previamente los protocolos configurados.

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.

MessagePack peculiaridades
Hay algunos problemas que tenga en cuenta cuando se usa el protocolo de concentrador MessagePack.
MessagePack distingue mayúsculas de minúsculas
El protocolo MessagePack distingue mayúsculas de minúsculas. Por ejemplo, considere la siguiente C# clase:

public class ChatMessage


{
public string Sender { get; }
public string Message { get; }
}

Cuando se envían desde el cliente de JavaScript, debe usar PascalCased nombres de propiedad, ya que deben
coincidir con las mayúsculas y minúsculas del C# clase exactamente. Por ejemplo:

connection.invoke("SomeMethod", { Sender: "Sally", Message: "Hello!" });

Uso de camelCased nombres correctamente no se enlazará a la C# clase. Puede solucionar esto mediante el uso
de la Key atributo para especificar un nombre diferente para la propiedad MessagePack. Para obtener más
información, consulte la documentación de MessagePack-CSharp.
DateTime.Kind no se conserva al serializar o deserializar
El protocolo MessagePack no proporciona una manera de codificar el Kind valor de un DateTime . Como
resultado, al deserializar una fecha, el protocolo de Hub MessagePack supone que la fecha de entrada está en
formato UTC. Si está trabajando con DateTime valores de hora local, se recomienda convertir a UTC antes de
enviarlos. Convertirlos a la hora UTC a la hora local cuando los recibe.
Para obtener más información sobre esta limitación, vea GitHub problema aspnet/SignalR #2632.
DateTime.MinValue no es compatible con MessagePack en JavaScript
El msgpack5 biblioteca usada por el cliente de JavaScript de SignalR no es compatible con la timestamp96 tipo en
MessagePack. Este tipo se usa para codificar los valores de fecha muy grande (ya sea muy pronto en el pasado o
en el futuro muy lejano). El valor de DateTime.MinValue es January 1, 0001 que debe estar codificado en un
timestamp96 valor. Debido a esto, enviar DateTime.MinValue un JavaScript no se admite el cliente. Cuando
DateTime.MinValue es recibido por el cliente de JavaScript, se produce el error siguiente:

Uncaught Error: unable to find ext type 255 at decoder.js:427

Por lo general, DateTime.MinValue se usa para codificar un "Falta" o null valor. Si tiene que codificar ese valor en
MessagePack, usar una que acepta valores NULL DateTime valor ( DateTime? ) o codificar un independiente bool
valor que indica si la fecha está presente.
Para obtener más información sobre esta limitación, vea GitHub problema aspnet/SignalR #2228.
Compatibilidad con MessagePack en el entorno de compilación "ahead-of-time"
El MessagePack-CSharp biblioteca usada por el cliente de .NET y el servidor usa la generación de código para
optimizar la serialización. Como resultado, no se admite de forma predeterminada en entornos que usan las
compilación "ahead-of-time" (por ejemplo, Xamarin iOS o Unity). Es posible usar MessagePack en estos entornos
"generando previamente" el código del serializador/deserializador. Para obtener más información, consulte la
documentación de MessagePack-CSharp. Una vez que los serializadores generados previamente, puede
registrarlos mediante el delegado de configuración pasando a AddMessagePackProtocol :

services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
{
MessagePack.Resolvers.GeneratedResolver.Instance,
MessagePack.Resolvers.StandardResolver.Instance
};
});

Comprobaciones de tipo sean más estrictas en MessagePack


El protocolo de Hub JSON llevará a cabo las conversiones de tipos durante la deserialización. Por ejemplo, si el
objeto entrante tiene un valor de propiedad que es un número ( { foo: 42 } ), pero la propiedad en la clase de
.NET es de tipo string , se convertirá el valor. Sin embargo, MessagePack no realiza esta conversión y producirá
una excepción que se puede ver en los registros del lado servidor (y en la consola):

InvalidDataException: Error binding arguments. Make sure that the types of the provided values match the types
of the hub method being invoked.

Para obtener más información sobre esta limitación, vea GitHub problema aspnet/SignalR #2937.

Recursos relacionados
Primeros pasos
Cliente .NET
Cliente de JavaScript
Usar la transmisión por secuencias en ASP.NET Core
SignalR
07/06/2019 • 16 minutes to read • Edit Online

Por Brennan Conroy


SignalR de ASP.NET Core admite la transmisión del cliente al servidor y del servidor al cliente. Esto es útil para
escenarios que llegan los fragmentos de datos con el tiempo. La transmisión por secuencias, significará que se ha
enviado cada fragmento para el cliente o servidor tan pronto como se convierte en disponible, en lugar de esperar
todos los datos estén disponibles.
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 que llegan los fragmentos de datos 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 un concentrador para streaming


Un método de concentrador se convierte automáticamente en un método de concentrador de transmisión por
secuencias al regresar IAsyncEnumerable<T>, ChannelReader<T>, Task<IAsyncEnumerable<T>> , o
Task<ChannelReader<T>> .

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>> .
Streaming de servidor a cliente
Métodos de concentrador de streaming puede devolver IAsyncEnumerable<T> además ChannelReader<T> . La
manera más sencilla para devolver IAsyncEnumerable<T> está haciendo que el método de concentrador un método
de iterador asincrónico como se muestra en el ejemplo siguiente. Métodos de iterador de async de concentrador
pueden aceptar un CancellationToken parámetro que se desencadena cuando el cliente cancela la suscripción de la
secuencia. Métodos de iterador Async evitar problemas comunes con los canales, como no devolver el
ChannelReader lo suficientemente pronto o sale del método sin completar la ChannelWriter<T>.

NOTE
El ejemplo siguiente requiere C# 8.0 o posterior.
public class AsyncEnumerableHub : Hub
{
public async IAsyncEnumerable<int> Counter(
int count,
int delay,
CancellationToken cancellationToken)
{
for (var i = 0; i < count; i++)
{
// Check the cancellation token regularly so that the server will stop
// producing items if the client disconnects.
cancellationToken.ThrowIfCancellationRequested();

yield return i;

// Use the cancellationToken in other APIs that accept cancellation


// tokens so the cancellation can flow down to them.
await Task.Delay(delay, cancellationToken);
}
}
}

El ejemplo siguiente muestra los conceptos básicos de transmisión de datos al cliente utilizando los canales. Cada
vez que se escribe un objeto en el ChannelWriter<T>, el objeto inmediatamente se envía al cliente. Al final, el
ChannelWriter completada para indicar al cliente la secuencia está cerrada.

NOTE
Escribir en el ChannelWriter<T> en un subproceso en segundo plano y vuelva el ChannelReader tan pronto como sea
posible. Las demás invocaciones de concentrador se bloquean hasta que un ChannelReader se devuelve.
Encapsular la lógica en un try ... catch . Completar la Channel en el catch como fuera el catch para asegurarse de
que el centro de invocación del método se completó correctamente.
public ChannelReader<int> Counter(
int count,
int delay,
CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<int>();

// We don't want to await WriteItemsAsync, otherwise we'd end up waiting


// for all the items to be written before returning the channel back to
// the client.
_ = WriteItemsAsync(channel.Writer, count, delay, cancellationToken);

return channel.Reader;
}

private async Task WriteItemsAsync(


ChannelWriter<int> writer,
int count,
int delay,
CancellationToken cancellationToken)
{
Exception localException = null;
try
{
for (var i = 0; i < count; i++)
{
await writer.WriteAsync(i, cancellationToken);

// Use the cancellationToken in other APIs that accept cancellation


// tokens so the cancellation can flow down to them.
await Task.Delay(delay, cancellationToken);
}
}
catch (Exception ex)
{
localException = ex;
}

writer.Complete(localException);
}
public class StreamHub : Hub
{
public ChannelReader<int> Counter(
int count,
int delay,
CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<int>();

// We don't want to await WriteItemsAsync, otherwise we'd end up waiting


// for all the items to be written before returning the channel back to
// the client.
_ = WriteItemsAsync(channel.Writer, count, delay, cancellationToken);

return channel.Reader;
}

private async Task WriteItemsAsync(


ChannelWriter<int> writer,
int count,
int delay,
CancellationToken cancellationToken)
{
try
{
for (var i = 0; i < count; i++)
{
// Check the cancellation token regularly so that the server will stop
// producing items if the client disconnects.
cancellationToken.ThrowIfCancellationRequested();
await writer.WriteAsync(i);

// Use the cancellationToken in other APIs that accept cancellation


// tokens so the cancellation can flow down to them.
await Task.Delay(delay, cancellationToken);
}
}
catch (Exception ex)
{
writer.TryComplete(ex);
}

writer.TryComplete();
}
}
public class StreamHub : Hub
{
public ChannelReader<int> Counter(int count, int delay)
{
var channel = Channel.CreateUnbounded<int>();

// We don't want to await WriteItemsAsync, otherwise we'd end up waiting


// for all the items to be written before returning the channel back to
// the client.
_ = WriteItemsAsync(channel.Writer, count, delay);

return channel.Reader;
}

private async Task WriteItemsAsync(


ChannelWriter<int> writer,
int count,
int delay)
{
try
{
for (var i = 0; i < count; i++)
{
await writer.WriteAsync(i);
await Task.Delay(delay);
}
}
catch (Exception ex)
{
writer.TryComplete(ex);
}

writer.TryComplete();
}
}

Métodos de concentrador de servidor a cliente streaming pueden aceptar un CancellationToken parámetro que se
desencadena cuando el cliente cancela la suscripción de la secuencia. Use este token para detener la operación del
servidor y libere cualquier recurso si el cliente se desconecta antes del final de la secuencia.
Cliente a servidor de transmisión por secuencias
Un método de concentrador se convierte automáticamente en un método de concentrador de cliente a servidor
streaming cuando acepta uno o más objetos de tipo ChannelReader<T> o IAsyncEnumerable<T>. El ejemplo
siguiente muestra los conceptos básicos de lectura de transmisión por secuencias datos enviados desde el cliente.
Cada vez que el cliente se escribe en el ChannelWriter<T>, los datos se escriben en el ChannelReader en el
servidor desde el que está leyendo el método de concentrador.

public async Task UploadStream(ChannelReader<string> stream)


{
while (await stream.WaitToReadAsync())
{
while (stream.TryRead(out var item))
{
// do something with the stream item
Console.WriteLine(item);
}
}
}

Un IAsyncEnumerable<T> sigue la versión del método.


NOTE
El ejemplo siguiente requiere C# 8.0 o posterior.

public async Task UploadStream(IAsyncEnumerable<Stream> stream)


{
await foreach (var item in stream)
{
Console.WriteLine(item);
}
}

Cliente .NET
Streaming de servidor a cliente
El StreamAsync y StreamAsChannelAsync métodos en HubConnection se usan para invocar métodos de transmisión
por secuencias con el cliente y el servidor. Pase el nombre del método de concentrador y los argumentos definidos
en el método de concentrador a StreamAsync o StreamAsChannelAsync . El parámetro genérico en StreamAsync<T> y
StreamAsChannelAsync<T> especifica el tipo de objetos devueltos por el método de transmisión por secuencias. Un
objeto de tipo IAsyncEnumerable<T> o ChannelReader<T> se devuelve desde la invocación de la secuencia y
representa el flujo en el cliente.
Un StreamAsync ejemplo que devuelve IAsyncEnumerable<int> :

// Call "Cancel" on this CancellationTokenSource to send a cancellation message to


// the server, which will trigger the corresponding token in the hub method.
var cancellationTokenSource = new CancellationTokenSource();
var stream = await hubConnection.StreamAsync<int>(
"Counter", 10, 500, cancellationTokenSource.Token);

await foreach (var count in stream)


{
Console.WriteLine($"{count}");
}

Console.WriteLine("Streaming completed");

Correspondiente StreamAsChannelAsync ejemplo que devuelve ChannelReader<int> :

// Call "Cancel" on this CancellationTokenSource to send a cancellation message to


// the server, which will trigger the corresponding token in the hub method.
var cancellationTokenSource = new CancellationTokenSource();
var channel = await hubConnection.StreamAsChannelAsync<int>(
"Counter", 10, 500, cancellationTokenSource.Token);

// 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");
El StreamAsChannelAsync método HubConnection se utiliza para invocar un método de transmisión por secuencias
del servidor al cliente. 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.

// Call "Cancel" on this CancellationTokenSource to send a cancellation message to


// the server, which will trigger the corresponding token in the hub method.
var cancellationTokenSource = new CancellationTokenSource();
var channel = await hubConnection.StreamAsChannelAsync<int>(
"Counter", 10, 500, cancellationTokenSource.Token);

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

El StreamAsChannelAsync método HubConnection se utiliza para invocar un método de transmisión por secuencias
del servidor al cliente. 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.

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 a servidor de transmisión por secuencias


Hay dos maneras de invocar un método de concentrador de cliente a servidor streaming desde el cliente. NET.
Puede que pase un IAsyncEnumerable<T> o un ChannelReader como argumento a SendAsync , InvokeAsync , o
StreamAsChannelAsync , según el método de concentrador que se invoca.

Cada vez que se escriben datos en el IAsyncEnumerable o ChannelWriter de objeto, el método de concentrador en
el servidor recibe un nuevo elemento con los datos del cliente.
Si usa un IAsyncEnumerable de objeto, finalice la secuencia después del método de devolución de elementos de
flujo se cierra.
NOTE
El ejemplo siguiente requiere C# 8.0 o posterior.

async IAsyncEnumerable<string> clientStreamData()


{
for (var i = 0; i < 5; i++)
{
var data = await FetchSomeData();
yield return data;
}
//After the for loop has completed and the local function exits the stream completion will be sent.
}

await connection.SendAsync("UploadStream", clientStreamData());

O si usa un ChannelWriter , complete el canal con channel.Writer.Complete() :

var channel = Channel.CreateBounded<string>(10);


await connection.SendAsync("UploadStream", channel.Reader);
await channel.Writer.WriteAsync("some data");
await channel.Writer.WriteAsync("some more data");
channel.Writer.Complete();

Cliente de JavaScript
Streaming de servidor a cliente
Los clientes de JavaScript llamar a métodos de transmisión por secuencias de servidor a cliente en los centros con
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 desde el
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 desde el cliente, llame a la dispose método en el ISubscription que se devuelve desde
el subscribe método. Llamar a este método produce la cancelación de la CancellationToken parámetro del
método de concentrador, si se proporciona uno.

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 desde el cliente, llame a la dispose método en el ISubscription que se devuelve desde
el subscribe método.
Cliente a servidor de transmisión por secuencias
Los clientes JavaScript llamar a métodos de transmisión por secuencias de cliente a servidor en los centros de
pasando un Subject como argumento a send , invoke , o stream , según el método de concentrador que se
invoca. El Subject es una clase que es similar a un Subject . Por ejemplo en RxJS, puede usar el asunto clase a
partir de esa biblioteca.
const subject = new signalR.Subject();
yield connection.send("UploadStream", subject);
var iteration = 0;
const intervalHandle = setInterval(() => {
iteration++;
subject.next(iteration.toString());
if (iteration === 10) {
clearInterval(intervalHandle);
subject.complete();
}
}, 500);

Una llamada a subject.next(item) con un elemento, escribe el elemento en la secuencia y el método de


concentrador recibe el elemento en el servidor.
Para finalizar el flujo, llame a subject.complete() .

Cliente de Java
Streaming de servidor a cliente
El cliente de SignalR Java usa el stream método para invocar métodos de transmisión por secuencias. stream
acepta tres o más argumentos:
El tipo esperado de los elementos de la secuencia.
El nombre del método de concentrador.
Argumentos definidos en el método de concentrador.

hubConnection.stream(String.class, "ExampleStreamingHubMethod", "Arg1")


.subscribe(
(item) -> {/* Define your onNext handler here. */ },
(error) -> {/* Define your onError handler here. */},
() -> {/* Define your onCompleted handler here. */});

El stream método HubConnection devuelve un objeto Observable del tipo de elemento de secuencia. El tipo
Observable subscribe método es donde onNext , onError y onCompleted se definen los controladores.

Recursos adicionales
Concentradores
Cliente .NET
Cliente de JavaScript
Publicar en Azure
Diferencias entre SignalR de ASP.NET y ASP.NET Core
SignalR
24/06/2019 • 8 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

Java Client Repositorio de GitHub (en desuso) Maven package com.microsoft.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
Reconexiones automática no se admiten en ASP.NET Core SignalR. Si el cliente está desconectado, el usuario debe
iniciar explícitamente una nueva conexión si desean volver a conectar. En ASP.NET SignalR, SignalR intenta volver
a conectarse al servidor si se interrumpe la conexión.
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.
Transportes
No se admite el transporte para siempre el marco de ASP.NET Core SignalR.

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

Sesiones permanentes
El modelo de escalabilidad horizontal de SignalR de ASP.NET permite a los clientes volver a conectarse y enviar
mensajes a cualquier servidor de la granja de servidores. En ASP.NET Core SignalR, el cliente debe interactuar con
el mismo servidor para la duración de la conexión. Para el escalado horizontal con Redis, que significa que se
requieren sesiones permanentes. Para usar el escalado horizontal Azure SignalR Service, sesiones temporales no
son necesarias porque el servicio controla las conexiones a los clientes.
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.
Eliminación de PersistentConnection
En ASP.NET Core SignalR, el PersistentConnection ha quitado la clase.
GlobalHost
ASP.NET Core tiene la inserción de dependencias (DI) integrada en el marco de trabajo. Los servicios pueden usar
DI para tener acceso a la HubContext. El GlobalHost objeto que se usa en ASP.NET SignalR para obtener un
HubContext no existe en ASP.NET Core SignalR.

HubPipeline
ASP.NET Core SignalR no tiene compatibilidad con HubPipeline módulos.

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.
Soporte técnico para Internet Explorer
ASP.NET Core SignalR requiere Microsoft Internet Explorer 11 o posterior (SignalR de ASP.NET admite Microsoft
Internet Explorer 8 y versiones posterior).
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
Backplane de Redis

Recursos adicionales
Concentradores
Cliente de JavaScript
Cliente .NET
Plataformas compatibles
Compatibilidad con WebSockets en ASP.NET Core
11/06/2019 • 15 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). Cómo ejecutar.

SignalR
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.
Para la mayoría de las aplicaciones, se recomienda SignalR en lugar de WebSockets sin procesar. SignalR
proporciona transporte de reserva para entornos donde WebSockets no está disponible. También proporciona un
modelo simple de aplicaciones de llamada a procedimiento remoto. Y, en la mayoría de los escenarios, SignalR
no tiene ninguna desventaja significativa de rendimiento en comparación a los WebSockets sin procesar.

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 (consulte 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.

Detección de
Instale el paquete Microsoft.AspNetCore.WebSockets.

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. El valor predeterminado es de dos minutos.
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. El valor predeterminado
es 4 KB.
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. El valor predeterminado es de dos minutos.
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. El valor predeterminado
es 4 KB.
AllowedOrigins - Una lista de valores de encabezado de origen permitidos para las solicitudes WebSocket. De
forma predeterminada, se permiten todos los orígenes. Consulte "Restricción de los orígenes de WebSocket" a
continuación para obtener información detallada.

var webSocketOptions = new WebSocketOptions()


{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};

app.UseWebSockets(webSocketOptions);

Aceptar solicitudes WebSocket


En algún momento posterior del ciclo de solicitudes (más adelante en el método Configure o en un método de
acción, 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 .
Cuando se usa un WebSocket, debe mantener la canalización de middleware en ejecución durante la duración de
la conexión. Si intenta enviar o recibir un mensaje de WebSocket después de que finalice la canalización de
middleware, es posible que obtenga una excepción como la siguiente:

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection


without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response
body, the response has completed.
Object name: 'HttpResponseStream'.

Si utiliza un servicio en segundo plano para escribir datos en un WebSocket, asegúrese de mantener en ejecución
el canal de middleware. Para ello, utilice un TaskCompletionSource<TResult>. Pase TaskCompletionSource a su
servicio de segundo plano y pídale que llame a TrySetResult cuando termine con WebSocket. Después, espere
(con await ) la propiedad Task durante la solicitud, como se muestra en el ejemplo siguiente:

app.Use(async (context, next) => {


var socket = await context.WebSockets.AcceptWebSocketAsync();
var socketFinishedTcs = new TaskCompletionSource<object>();

BackgroundSocketProcessor.AddSocket(socket, socketFinishedTcs);

await socketFinishedTcs.Task;
});

La excepción de cierre de WebSocket también puede ocurrir si la devolución de un método de acción se produce
demasiado pronto. Si acepta un socket en un método de acción, espere a que finalice el código que utiliza el
socket antes de volver del método de acción.
No use nunca Task.Wait() , Task.Result ni llamadas de bloqueo similares para esperar a que se complete el
socket, ya que pueden causar graves problemas de subprocesamiento. Use siempre await .

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.

Control de las desconexiones del cliente


No se informa automáticamente al servidor cuando el cliente se desconecta debido a la pérdida de conectividad.
El servidor recibe un mensaje de desconexión solo si el cliente lo envía, acción que no se puede realizar si se
pierde la conexión a Internet. Si desea realizar alguna acción cuando eso suceda, establezca un tiempo de
expiración después de que no se reciba del cliente dentro de un determinado período.
Si el cliente no siempre está enviando mensajes y no quiere que se agote el tiempo de expiración solo porque la
conexión está inactiva, haga que el cliente utilice un temporizador para enviar un mensaje de ping cada equis
segundos. En el servidor, si aún no ha llegado un mensaje dentro de 2*X segundos después del anterior, termine
la conexión e informe que ha desconectado el cliente. Espere el doble del intervalo de tiempo esperado para dejar
tiempo extra para los retrasos de la red que podrían retener el mensaje de ping.

Restricción de los orígenes de WebSocket


Las protecciones proporcionadas por CORS no se aplican a WebSockets. Los exploradores no hacen lo siguiente:
Efectúan solicitudes preparatorias CORS.
Respetan las restricciones especificadas en los encabezados Access-Control al efectuar solicitudes de
WebSocket.
En cambio, sí que envían el encabezado Origin al emitir solicitudes de WebSocket. Las aplicaciones deben
configurarse para validar estos encabezados a fin de garantizar que solo se permitan los WebSockets
procedentes de los orígenes esperados.
Si hospeda su servidor en "https://server.com" y su cliente en "https://client.com", agregue "https://client.com" a
la lista AllowedOrigins de WebSockets para efectuar la comprobación.

var webSocketOptions = new WebSocketOptions()


{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

NOTE
El encabezado Origin está controlado por el cliente y, al igual que el encabezado Referer , se puede falsificar. No use
estos encabezados como mecanismo de autenticació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.

NOTE
WebSocket siempre está habilitado cuando se usa IIS Express.
Habilitación de WebSocket en IIS
Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 2012 o posterior:

NOTE
Estos pasos no son necesarios cuando se usa IIS Express

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:

NOTE
Estos pasos no son necesarios cuando se usa IIS Express

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 nodos siguientes: 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>

Aplicación de ejemplo
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.
Registro y diagnóstico de ASP.NET Core SignalR
24/06/2019 • 15 minutes to read • Edit Online

Por Andrew Stanton-Nurse


Este artículo proporcionan instrucciones para recopilar diagnósticos desde la aplicación de ASP.NET Core SignalR
para ayudar a solucionar problemas.

Registro del lado servidor


WARNING
Registros de servidor pueden contener información confidencial de su aplicación. Nunca publicar los registros sin procesar de
las aplicaciones de producción en los foros públicos como GitHub.

Dado que SignalR forma parte de ASP.NET Core, se usa el sistema de registro de ASP.NET Core. En la
configuración predeterminada, los registros de SignalR información muy poco, pero esto se puede configurar.
Consulte la documentación sobre registro de ASP.NET Core para obtener más información sobre cómo
configurar el registro de ASP.NET Core.
SignalR usa dos categorías de registrador:
Microsoft.AspNetCore.SignalR – para los registros relacionados con los protocolos de concentrador, activación
de concentradores, invocar métodos y otras actividades relacionadas con el concentrador.
Microsoft.AspNetCore.Http.Connections – para los registros relacionados con los transportes como
WebSockets, Long Polling y los eventos y la infraestructura de SignalR bajo nivel.
Para habilitar los registros detallados de SignalR, configure ambos prefijos anteriores a la Debug nivel en su
appsettings.json archivo agregando los elementos siguientes en el LogLevel subsección en Logging :

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore.SignalR": "Debug",
"Microsoft.AspNetCore.Http.Connections": "Debug"
}
}
}

También puede configurar esto en el código en su CreateWebHostBuilder método:

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


WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
})
.UseStartup<Startup>();
Si no utiliza la configuración basada en JSON, establezca los siguientes valores de configuración en el sistema de
configuración:
Logging:LogLevel:Microsoft.AspNetCore.SignalR = Debug
Logging:LogLevel:Microsoft.AspNetCore.Http.Connections = Debug

Consulte la documentación de su sistema de configuración determinar cómo especificar los valores de


configuración anidados. Por ejemplo, al usar variables de entorno, dos _ caracteres se usan en lugar de la : (por
ejemplo, Logging__LogLevel__Microsoft.AspNetCore.SignalR ).
Se recomienda usar la Debug al recopilar más detallada de diagnóstico para la aplicación de nivel. El Trace nivel
produce el diagnóstico de nivel muy bajo y rara vez se necesitan para diagnosticar problemas en la aplicación.

Acceso a los registros de servidor


El acceso a los registros del lado servidor depende del entorno en el que está ejecutando.
Como una aplicación de consola fuera de IIS
Si está ejecutando en una aplicación de consola, el registrador de consola debe habilitarse de forma
predeterminada. Los registros de SignalR aparecerá en la consola.
Dentro de IIS Express de Visual Studio
Visual Studio muestra la salida del registro en el salida ventana. Seleccione el servidor Web de ASP.NET Core
lista desplegable de la opción.
Azure App Service
Habilitar la registro de la aplicación (Filesystem ) opción el los registros de diagnóstico sección del portal de
Azure App Service y configurar el nivel a Verbose . Los registros deben estar disponibles desde el secuencias de
registro servicio y los registros del sistema de archivos de App Service. Para obtener más información, consulte
secuencias de registro Azure.
Otros entornos
Si la aplicación se implementa en otro entorno (por ejemplo, Docker, Kubernetes o el servicio de Windows),
consulte Registro en ASP.NET Core para obtener más información sobre cómo configurar los proveedores de
registro adecuados para el entorno.

Registro de cliente de JavaScript


WARNING
Registros del lado cliente pueden contener información confidencial de la aplicación. Nunca publicar los registros sin procesar
de las aplicaciones de producción en los foros públicos como GitHub.

Cuando se usa el cliente de JavaScript, puede configurar las opciones de registro mediante el configureLogging
método HubConnectionBuilder :

let connection = new signalR.HubConnectionBuilder()


.withUrl("/my/hub/url")
.configureLogging(signalR.LogLevel.Debug)
.build();

Para deshabilitar el registro por completo, especifique signalR.LogLevel.None en el configureLogging método.


La siguiente tabla muestra los niveles de registro disponibles para el cliente de JavaScript. Establecer el nivel de
registro en uno de estos valores, habilita el registro en ese nivel y todos los niveles por encima de él en la tabla.

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.

Una vez haya configurado el nivel de detalle, los registros se escribirán en la consola del explorador (o la salida
estándar en una aplicación de NodeJS ).
Si desea enviar registros a un sistema de registro personalizado, puede proporcionar un objeto de JavaScript que
implementa el ILogger interfaz. Es el único método que debe implementarse log , que toma el nivel del evento y
el mensaje asociado al evento. Por ejemplo:

import { ILogger, LogLevel, HubConnectionBuilder } from "@aspnet/signalr";

export class MyLogger implements ILogger {


log(logLevel: LogLevel, message: string) {
// Use `message` and `logLevel` to record the log message to your own system
}
}

// later on, when configuring your connection...

let connection = new HubConnectionBuilder()


.withUrl("/my/hub/url")
.configureLogging(new MyLogger())
.build();

Registro de cliente de .NET


WARNING
Registros del lado cliente pueden contener información confidencial de la aplicación. Nunca publicar los registros sin procesar
de las aplicaciones de producción en los foros públicos como GitHub.

Para obtener los registros del cliente. NET, puede usar el ConfigureLogging método HubConnectionBuilder . Esto
funciona del mismo modo que el ConfigureLogging método WebHostBuilder y HostBuilder . Puede configurar los
mismos proveedores de registro que usa en ASP.NET Core. Sin embargo, deberá instalar manualmente y habilitar
los paquetes de NuGet para los proveedores de registro individuales.
Registro de la consola
Para habilitar el registro de consola, agregue el Microsoft.Extensions.Logging.Console paquete. A continuación,
utilice el AddConsole método para configurar el registrador de consola:

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/my/hub/url")
.ConfigureLogging(logging =>
{
// Log to the Console
logging.AddConsole();

// This will set ALL logging to Debug level


logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();

El registro de la ventana de salida de depuración


También puede configurar registros para ir a la salida ventana de Visual Studio. Instalar el
Microsoft.Extensions.Logging.Debug empaquetar y utilizar el AddDebug método:

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/my/hub/url")
.ConfigureLogging(logging =>
{
// Log to the Output Window
logging.AddDebug();

// This will set ALL logging to Debug level


logging.SetMinimumLevel(LogLevel.Debug)
})
.Build();

Otros proveedores de registro


SignalR admite otros proveedores de registro como Seq, Serilog, NLog o cualquier otro sistema de registro que
se integra con Microsoft.Extensions.Logging . Si su sistema de registro proporciona una ILoggerProvider , puede
registrarlo con AddProvider :

var connection = new HubConnectionBuilder()


.WithUrl("https://example.com/my/hub/url")
.ConfigureLogging(logging =>
{
// Log to your custom provider
logging.AddProvider(new MyCustomLoggingProvider());

// This will set ALL logging to Debug level


logging.SetMinimumLevel(LogLevel.Debug)
})
.Build();

Nivel de detalle de control


Si está iniciando sesión desde otros lugares en su aplicación, cambiar el nivel predeterminado que Debug puede
ser demasiado detallado. Puede utilizar un filtro para configurar el nivel de registro para los registros de SignalR.
Esto puede hacerse en el código, casi la misma manera que en el servidor:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/my/hub/url")
.ConfigureLogging(logging =>
{
// Register your providers

// Set the default log level to Information, but to Debug for SignalR-related loggers.
logging.SetMinimumLevel(LogLevel.Information);
logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
})
.Build();

Rastros de red
WARNING
Un seguimiento de red contiene todo el contenido de todos los mensajes enviados por la aplicación. Nunca publicar los
rastros de red sin procesar de aplicaciones de producción en los foros públicos como GitHub.

Si encuentra algún problema, un seguimiento de red en ocasiones, puede proporcionar mucha información útil.
Esto es especialmente útil si va a informar de un problema en nuestro rastreador de problemas.

Recopilar un seguimiento de red con Fiddler (opción preferida)


Este método funciona para todas las aplicaciones.
Fiddler es una herramienta muy eficaz para recopilar seguimientos HTTP. Instálelo desde telerik.com/fiddler,
iniciarla y, a continuación, ejecutar la aplicación y reproduzca el problema. Fiddler está disponible para Windows, y
existen versiones beta para macOS y Linux.
Si se conecta mediante HTTPS, hay algunos pasos adicionales para asegurarse de que Fiddler puede descifrar el
tráfico HTTPS. Para obtener más información, consulte el documentación de Fiddler.
Una vez que haya recopilado el seguimiento, puede exportar el seguimiento seleccionando archivo > guardar >
todas las sesiones desde la barra de menús.
Recopilar un seguimiento de red con tcpdump (macOS y Linux solo)
Este método funciona para todas las aplicaciones.
Puede recopilar seguimientos TCP sin formato mediante tcpdump ejecutando el siguiente comando desde un shell
de comandos. Es posible que deba ser root o un prefijo con el comando de sudo si se produce un error de
permisos:

tcpdump -i [interface] -w trace.pcap

Reemplace [interface] con el que desea capturar en la interfaz de red. Normalmente, esto es algo parecido a
/dev/eth0 (para la interfaz Ethernet estándar ) o /dev/lo0 (para el tráfico de localhost). Para obtener más
información, consulte la tcpdump página man en el sistema host.

Recopilar un seguimiento de red en el explorador


Este método solo funciona para las aplicaciones basadas en explorador.
Mayoría de las herramientas de desarrollador de explorador tiene una pestaña de "Red" que le permite capturar la
actividad de red entre el explorador y el servidor. Sin embargo, estos seguimientos no incluyen los mensajes de
WebSocket y los eventos. Si utiliza estos transportes, mediante una herramienta como Fiddler o TcpDump
(descrito a continuación) es un enfoque más adecuado.
Microsoft Edge e Internet Explorer
(Las instrucciones son los mismos para Edge e Internet Explorer)
1. Presione F12 para abrir las herramientas de desarrollo
2. Haga clic en la ficha red
3. Actualice la página (si es necesario) y reproducir el problema
4. Haga clic en el icono Guardar en la barra de herramientas para exportar el seguimiento como un archivo
"HAR":
Google Chrome
1. Presione F12 para abrir las herramientas de desarrollo
2. Haga clic en la ficha red
3. Actualice la página (si es necesario) y reproducir el problema
4. Haga clic en cualquier lugar en la lista de solicitudes y elija "Guardar como HAR con contenido":

Mozilla Firefox
1. Presione F12 para abrir las herramientas de desarrollo
2. Haga clic en la ficha red
3. Actualice la página (si es necesario) y reproducir el problema
4. Haga clic en cualquier lugar en la lista de solicitudes y elija "Guardar todo como HAR"
Adjuntar archivos de diagnóstico de problemas de GitHub
Puede adjuntar archivos de diagnóstico a problemas de GitHub cambiando por lo que tienen un .txt extensión y,
a continuación, arrastrándolos y colocándolos en el problema.

NOTE
No pegue el contenido de los archivos de registro o los rastros de red en un problema de GitHub. Estos registros y
seguimientos pueden ser bastante grandes y GitHub normalmente los trunca.

Recursos adicionales
Configuración de ASP.NET Core SignalR
Cliente ASP.NET Core SignalR JavaScript
Cliente de .NET de ASP.NET Core SignalR
Introducción a gRPC en ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

Por John Luo


gRPC es un marco de llamada a procedimiento remoto (RPC ) de alto rendimiento e independiente del idioma.
Para obtener más información sobre los aspectos básicos de gRPC, vea la página de documentación de gRPC.
Las principales ventajas de gRPC son:
Marco de RPC moderno, ligero y de alto rendimiento.
Desarrollo de la API de primer contrato utilizando búferes de protocolo de forma predeterminada, lo que
permite realizar implementaciones independientes del idioma.
Dispone de herramientas para muchos idioma con la finalidad de generar clientes y servidores fuertemente
tipados.
Admite llamadas de transmisión en secuencias bidireccionales, de servidor y de cliente.
Uso reducido de red con serialización binaria Protobuf.
Estas ventajas hacen que gRPC sea ideal para:
Microservicios ligeros en los que la eficiencia sea fundamental.
Sistemas políglotas en los que se requieran varios idiomas para el desarrollo.
Servicios en tiempo real de punto a punto que necesitan controlar respuestas o solicitudes de transmisión en
secuencias.
Mientras que C# se puede implementar desde la página de gRPC oficial, la implementación actual se basa en la
biblioteca nativa escrita en C (gRPC C -core). Estamos trabajando para proporcionar una nueva implementación
totalmente administrada basada en el servidor HTTP de Kestrel y la pila de ASP.NET Core. En los siguientes
documentos encontrará una introducción a la creación de servicios de gRPC con esta nueva implementación.

Recursos adicionales
Servicios gRPC con C#
Creación de un servidor y un cliente gRPC en ASP.NET Core
Servicios gRPC con ASP.NET Core
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
gRPC servicios con c##
18/06/2019 • 5 minutes to read • Edit Online

Este documento describen los conceptos necesarios para escribir gRPC aplicaciones en C#. Los temas tratados
aquí se aplican a ambos C -core-aplicaciones basadas en ASP.NET Core y basada en gRPC.

archivo proto
gRPC usa un enfoque de contrato primero al desarrollo de API. Los búferes de protocolo (protobuf) se usan como
el lenguaje de diseño de interfaz (IDL ) de forma predeterminada. El .proto contiene el archivo:
La definición del servicio gRPC.
Los mensajes enviados entre clientes y servidores.
Para obtener más información sobre la sintaxis de los archivos protobuf, consulte el documentación oficial
(protobuf).
Por ejemplo, considere la greet.proto archivo usado en Introducción gRPC service:
Define un Greeter service.
El Greeter servicio define un SayHello llamar.
SayHello envía un HelloRequest del mensaje y recibe un HelloResponse mensaje:

syntax = "proto3";

package Greet;

// The greeting service definition.


service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.


message HelloRequest {
string name = 1;
}

// The response message containing the greetings.


message HelloReply {
string message = 1;
}

Agregar un archivo .proto a una C# app


El .proto archivo está incluido en un proyecto agregándolo a la <Protobuf> grupo de elementos:

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server"/>
</ItemGroup>

C#Compatibilidad con herramientas de .proto archivos


El paquete de herramientas Grpc.Tools es necesaria para generar el C# activos desde .proto archivos. Los recursos
generados (archivos):
Se generan según sea necesario cada vez que se compila el proyecto.
No se agrega al proyecto o proteger en el control de código fuente.
Son un artefacto de compilación contenido en el obj directory.
Este paquete es necesario para los proyectos de servidor y cliente. Grpc.Tools se pueden agregar mediante el
Administrador de paquetes en Visual Studio o agregando un <PackageReference> al archivo de proyecto:

<PackageReference Include="Grpc.Tools" Version="1.21.0" PrivateAssets="All" />

El paquete de herramientas no es necesario en el runtime, de modo que la dependencia se marca con


PrivateAssets="All" .

Genera C# activos
El paquete de herramientas genera la C# tipos que representan los mensajes definidos en el que se incluyen
.proto archivos.
Para los recursos de servidor, se genera un tipo base abstracto de servicio. El tipo base contiene las definiciones de
todas las llamadas gRPC contenidas en el .proto archivo. Crear una implementación de servicio concreta que se
deriva este tipo base e implementa la lógica para las llamadas gRPC. Para el greet.proto , en el ejemplo se ha
descrito anteriormente, abstracta GreeterBase tipo que contiene un virtual SayHello se genera el método. Una
implementación concreta GreeterService invalida el método e implementa la lógica que administra la llamada
gRPC.

public class GreeterService : Greeter.GreeterBase


{
public override Task<HelloReply> SayHello(
HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}

Para los recursos del lado cliente, se genera un tipo concreto de cliente. Llama la gRPC el .proto archivo se
traducen a métodos en el tipo concreto, que se puede llamar. Para el greet.proto , en el ejemplo se ha descrito
anteriormente, un hormigón GreeterClient tipo se genera. Llame a GreeterClient.SayHello para iniciar una
llamada gRPC al servidor.
static async Task Main(string[] args)
{
AppContext.SetSwitch(
"System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
true);
var httpClient = new HttpClient();
// The port number(50051) must match the port of the gRPC server.
httpClient.BaseAddress = new Uri("http://localhost:50051");
var client = GrpcClient.Create<Greeter.GreeterClient>(httpClient);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}

De forma predeterminada, los recursos de servidor y cliente se generan para cada .proto archivo incluido en el
<Protobuf> grupo de elementos. Para asegurarse de que solo los recursos de servidor se generan en un proyecto
de servidor, el GrpcServices atributo está establecido en Server .

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server"/>
</ItemGroup>

De forma similar, el atributo está establecido en Client en proyectos de cliente.

Recursos adicionales
Introducción a gRPC en ASP.NET Core
Creación de un servidor y un cliente gRPC en ASP.NET Core
Servicios gRPC con ASP.NET Core
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
Servicios gRPC con ASP.NET Core
03/07/2019 • 4 minutes to read • Edit Online

Este documento muestra cómo empezar a trabajar con servicios gRPC mediante ASP.NET Core.

Requisitos previos
Visual Studio
Visual Studio Code
Visual Studio para Mac
Visual Studio de 2019 con el ASP.NET y desarrollo web carga de trabajo
Obtener una vista previa de .NET core SDK 3.0

Introducción al servicio gRPC en ASP.NET Core


Vea o descargue el código de ejemplo (cómo descargarlo).
Visual Studio
Visual Studio Code/Visual Studio para Mac
Consulte empezar a trabajar con servicios gRPC para obtener instrucciones detalladas sobre cómo crear un
proyecto gRPC.

Agregar servicios de gRPC a una aplicación ASP.NET Core


gRPC requiere los siguientes paquetes:
Grpc.AspNetCore.Server
Google.Protobuf para protobuf las API del mensaje.
Grpc.Tools
Configurar gRPC
gRPC está habilitado con el AddGrpc método:
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.AddGrpc();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
// Communication with gRPC endpoints must be made through a gRPC client.
// To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
endpoints.MapGrpcService<GreeterService>();
});
}
}

Cada servicio gRPC se agrega a la canalización de enrutamiento a través de la MapGrpcService método:

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.AddGrpc();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
// Communication with gRPC endpoints must be made through a gRPC client.
// To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
endpoints.MapGrpcService<GreeterService>();
});
}
}

Características y middleware de ASP.NET Core comparten la canalización de enrutamiento, por lo tanto, una
aplicación puede configurarse para dar servicio a los controladores de solicitudes adicionales. Los controladores
de solicitud adicionales, como controladores de MVC, trabajaran en paralelo con los servicios configurados gRPC.
Integración con API de ASP.NET Core
gRPC services tienen acceso completo a las características de ASP.NET Core como inserción de dependencias (DI)
y registro. Por ejemplo, la implementación del servicio puede resolver un servicio de registrador desde el
contenedor de DI a través del constructor:

public class GreeterService : Greeter.GreeterBase


{
public GreeterService(ILogger<GreeterService> logger)
{
}
}

De forma predeterminada, la implementación del servicio gRPC puede resolver otros servicios de inserción de
dependencias con cualquier duración (Singleton, en el ámbito o transitorio).
Resolver HttpContext en gRPC métodos
La API gRPC proporciona acceso a algunos datos de mensaje HTTP/2, como el método, host, encabezado y
finalizadores. Es el acceso a través de la ServerCallContext argumento pasado a cada método gRPC:

public class GreeterService : Greeter.GreeterBase


{
public override Task<HelloReply> SayHello(
HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}

ServerCallContext no proporciona acceso completo a HttpContext en todas las APIs ASP.NET. El GetHttpContext
método de extensión proporciona acceso completo a la HttpContext que representa el mensaje de HTTP/2
subyacente en ASP.NET APIs:

public class GreeterService : Greeter.GreeterBase


{
public override Task<HelloReply> SayHello(
HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}

Recursos adicionales
Creación de un servidor y un cliente gRPC en ASP.NET Core
Introducción a gRPC en ASP.NET Core
Servicios gRPC con C#
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
gRPC para la configuración de ASP.NET Core
04/06/2019 • 3 minutes to read • Edit Online

Configurar las opciones de servicios


En la tabla siguiente se describe las opciones de configuración de servicios de gRPC:

OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

SendMaxMessageSize null El tamaño máximo del mensaje en bytes


que se pueden enviar desde el servidor.
Intentando enviar un mensaje que
supera los resultados de tamaño
máximo de mensaje configurado en una
excepción.

ReceiveMaxMessageSize 4 MB El tamaño máximo del mensaje en


bytes, que puede ser recibido por el
servidor. Si el servidor recibe un
mensaje que supera este límite, produce
una excepción. Al aumentar este valor
permite que el servidor recibir los
mensajes más grandes, pero puede
repercutir negativamente en el
consumo de memoria.

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 servicio. De manera
predeterminada, es false . Establecer
EnableDetailedErrors a true
puede producir la pérdida de
información confidencial.

CompressionProviders gzip Una colección de proveedores de


compresión utilizado para comprimir y
descomprimir los mensajes. Se pueden
crear proveedores de compresión
personalizado y agregados a la
colección. El valor predeterminado
configurado el proveedor admite gzip
compresión.

ResponseCompressionAlgorithm null El algoritmo de compresión utilizado


para comprimir los mensajes enviados
desde el servidor. El algoritmo debe
coincidir con un proveedor de
compresión en CompressionProviders
. Para que el algoritmo comprimir una
respuesta, el cliente debe indicar admite
el algoritmo enviando el grpc-
codificación aceptada encabezado.
OPCIÓN VALOR PREDETERMINADO DESCRIPCIÓN

ResponseCompressionLevel null El nivel de compresión para comprimir


los mensajes enviados desde el servidor.

Se pueden configurar opciones para todos los servicios al proporcionar un delegado de opciones para la AddGrpc
llamar Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
options.ReceiveMaxMessageSize = 2 * 1024 * 1024; // 2 megabytes
options.SendMaxMessageSize = 5 * 1024 * 1024; // 5 megabytes
});
}

Opciones para un único servicio invalidan las opciones globales de AddGrpc y pueden configurarse mediante
AddServiceOptions<TService> :

public void ConfigureServices(IServiceCollection services)


{
services.AddGrpc().AddServiceOptions<MyService>(options =>
{
options.ReceiveMaxMessageSize = 2 * 1024 * 1024;
options.SendMaxMessageSize = 5 * 1024 * 1024;
});
}

Configurar las opciones de cliente


El código siguiente establece el envío máximo de cliente y el tamaño de mensaje de recepción:

static async Task Main(string[] args)


{
var channel = new Channel("localhost:5001", ChannelCredentials.Insecure, new[]{
new ChannelOption(ChannelOptions.MaxSendMessageLength , 2*1024*1024),
new ChannelOption(ChannelOptions.MaxReceiveMessageLength , 5 *1024*1024)
});
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
await channel.ShutdownAsync();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}

Recursos adicionales
Creación de un servidor y un cliente gRPC en ASP.NET Core
Introducción a gRPC en ASP.NET Core
Servicios gRPC con C#
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
Migrar los servicios gRPC de núcleo de C a ASP.NET
Core
10/05/2019 • 4 minutes to read • Edit Online

Por John Luo


Debido a la implementación de la pila subyacente, no todas las características funcionan del mismo modo entre
basada en núcleo C gRPC aplicaciones y las aplicaciones basadas en ASP.NET Core. Este artículo destacan las
diferencias principales para migrar entre las dos pilas.

duración de implementación del servicio gRPC


En la pila de ASP.NET Core, servicios de gRPC, de forma predeterminada, se crean con un duración con ámbito.
En cambio, gRPC C -core de forma predeterminada se enlaza a un servicio con un duración de singleton.
Una duración con ámbito permite la implementación del servicio resolver otros servicios con la duración con
ámbito. Por ejemplo, también puede resolver una duración con ámbito DBContext desde el contenedor de DI a
través de la inserción del constructor. Uso de duración con ámbito:
Se construye una nueva instancia de la implementación del servicio para cada solicitud.
No se puede compartir el estado entre las solicitudes a través de los miembros de instancia en el tipo de
implementación.
La expectativa es almacenar los estados compartidos en un servicio de singleton en el contenedor de DI. Se
resuelven los estados compartidos almacenados en el constructor de la implementación del servicio gRPC.
Para obtener más información sobre la duración de los servicios, consulte Inserción de dependencias en ASP.NET
Core.
Agregar un servicio de singleton
Para facilitar la transición de una implementación de C -core gRPC a ASP.NET Core, es posible cambiar la
duración del servicio de la implementación del servicio de ámbito de singleton. Esto implica la adición de una
instancia de la implementación del servicio para el contenedor de DI:

public void ConfigureServices(IServiceCollection services)


{
services.AddGrpc();
services.AddSingleton(new GreeterService());
}

Sin embargo, una implementación de servicio con una duración de singleton ya no es capaz de resolver los
servicios con ámbito a través de la inserción del constructor.

Configurar las opciones de servicios gRPC


En aplicaciones basadas en C -core valores como grpc.max_receive_message_length y
grpc.max_send_message_length se configuran con ChannelOption cuando construir la instancia del servidor.
En ASP.NET Core, gRPC proporciona la configuración a través de la GrpcServiceOptions tipo. Por ejemplo, gRPC
de un servicio se puede configurar el tamaño máximo de mensaje entrante a través de AddGrpc :
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.ReceiveMaxMessageSize = 16384; // 16 MB
});
}

Para obtener más información sobre la configuración, consulte gRPC para la configuración de ASP.NET Core.

Registro
Las aplicaciones basada en núcleo C dependen de la GrpcEnvironment a configurar el registrador con fines de
depuración. La pila de ASP.NET Core proporciona esta funcionalidad a través de la API de registro. Por ejemplo,
un registrador puede agregarse al servicio gRPC mediante la inserción de constructor:

public class GreeterService : Greeter.GreeterBase


{
public GreeterService(ILogger<GreeterService> logger)
{
}
}

HTTPS
Configuración de aplicaciones basada en núcleo C HTTPS a través de la Server.Ports propiedad. Un concepto
similar se utiliza para configurar servidores de ASP.NET Core. Por ejemplo, se usa Kestrel configuración de
extremo para esta funcionalidad.

Los interceptores y Middleware


ASP.NET Core middleware ofrece funcionalidades similares en comparación con los interceptores de aplicaciones
basada en núcleo C gRPC. Middleware y los interceptores son conceptualmente iguales ya que ambos se usan
para construir una canalización que controla una solicitud gRPC. Ambos permiten que se realice antes o después
del siguiente componente de la canalización de trabajo. Sin embargo, middleware de ASP.NET Core funciona en
los mensajes subyacentes de HTTP/2, mientras que los interceptores de operan en la capa gRPC del uso de
abstracción del ServerCallContext.

Recursos adicionales
Introducción a gRPC en ASP.NET Core
Servicios gRPC con C#
Servicios gRPC con ASP.NET Core
Creación de un servidor y un cliente gRPC en ASP.NET Core
Comparación entre los servicios gRPC y las API HTTP
30/05/2019 • 10 minutes to read • Edit Online

Por James Newton-King


Este artículo se explica cómo gRPC servicios comparar a HTTP APIs (incluido ASP.NET Core API Web). La
tecnología utilizada para proporcionar una API para la aplicación es una opción importante y gRPC ofrece ventajas
exclusivas en comparación con HTTP APIs. En este artículo se describe los puntos fuertes y débiles de gRPC y
recomienda escenarios para usar gRPC a través de otras tecnologías.
Información general

CARACTERÍSTICA GRPC API DE HTTP CON JSON

Contrato Necesario ( *.proto ) Opcional (OpenAPI)

Transporte HTTP/2 HTTP

Payload Protobuf (binarios pequeños) JSON (grande, legible)

Prescriptiveness Especificación de Strict Flexible. Cualquier HTTP es válido

Streaming Cliente, servidor bidireccionales Cliente, servidor

Compatibilidad con exploradores No (requiere grpc web) Sí

Seguridad Transporte (HTTPS) Transporte (HTTPS)

Generación de código de cliente Sí OpenAPI + las herramientas de terceros

puntos fuertes de gRPC


Rendimiento
gRPC mensajes se serializan utilizando Protobuf, un formato de mensaje binario eficaz. Protobuf serializa muy
rápidamente en el servidor y cliente. Resultados de la serialización Protobuf en cargas de mensajes pequeños,
importantes en escenarios de ancho de banda limitado, como las aplicaciones móviles.
gRPC está diseñado para HTTP/2, una revisión principal de HTTP que proporciona ventajas significativas de
rendimiento a través de HTTP 1.x:
Tramas binaria y la compresión. Protocolo HTTP/2 es compacto y eficaz tanto en el envío y recepción.
Multiplexación de varias llamadas HTTP/2 a través de una única conexión TCP. Elimina la multiplexación head
de línea de bloqueo.
Generación de código
Todos los marcos de gRPC proporcionan soporte técnico de primera generación de código. Es un archivo principal
para desarrollo gRPC el *.proto archivo, que define el contrato de servicios de gRPC y mensajes. Desde este
marcos de trabajo de archivo gRPC código generará una clase base del servicio, mensajes y un cliente
completando.
Al compartir la *.proto archivo entre el servidor y cliente, los mensajes y código de cliente puede generarse desde
final al final. Generación de código del cliente elimina la duplicación de los mensajes en el cliente y servidor y crea
a un cliente fuertemente tipado para los usuarios. No hace falta escribir a un cliente ahorra el tiempo de desarrollo
importante en las aplicaciones con muchos servicios.
Especificación de Strict
No existe una especificación formal de la API de HTTP con JSON. Los desarrolladores debatir el mejor formato de
direcciones URL, los verbos HTTP y códigos de respuesta.
El gRPC especificación es preceptivo acerca del formato que debe seguir un servicio gRPC. gRPC elimina debate y
ahorra tiempo al desarrollador porque gPRC es coherente en las plataformas y las implementaciones.
Streaming
HTTP/2 proporciona una base para las secuencias de comunicación de larga duración, en tiempo real. gRPC
proporciona soporte de primera clase para streaming a través de HTTP/2.
Un servicio gRPC admite todas las combinaciones de streaming:
Unario (ninguna transmisión por secuencias)
Servidor a cliente de streaming
Cliente a servidor de streaming
Bidireccional de transmisión por secuencias
Fecha límite o tiempos de espera y la cancelación
gRPC permite a los clientes especificar cuánto están dispuestos a esperar una llamada RPC a completar. El fecha
límite se envía al servidor, y el servidor puede decidir qué acción realizar si supera la fecha límite. Por ejemplo, el
servidor puede cancelar las solicitudes de gRPC/HTTP/base de datos en curso en tiempo de espera.
Propagación de la fecha límite y la cancelación mediante el elemento secundario gRPC llamadas ayuda a exigir
límites de uso de recursos.

gRPC recomendada escenarios


gRPC se adapta perfectamente a los siguientes escenarios:
Microservicios – gRPC está diseñado para la comunicación de alto rendimiento y baja latencia. gRPC es
excelente para microservicios ligera donde la eficiencia es fundamental.
Comunicación en tiempo real de punto a punto – gRPC tiene excelente compatibilidad con streaming
bidireccionales. gRPC services pueden insertar mensajes que en tiempo real sin sondeo.
Entornos políglotas – gRPC herramientas es compatible con todos los lenguajes de desarrollo conocidas, lo
gRPC una buena elección para entornos de varios idiomas.
Entornos restringidos de red – gRPC mensajes se serializan con Protobuf, un formato de mensajes ligero. Un
mensaje gRPC siempre es menor que un mensaje JSON equivalente.

gRPC débiles
Compatibilidad con exploradores limitado
No podrá llamar directamente a un servicio gRPC desde un explorador hoy en día. gRPC principalmente usa
características HTTP/2 y ningún explorador proporciona el nivel de control necesario a través de las solicitudes
web para admitir a un cliente gRPC. Por ejemplo, los exploradores no permiten un autor de llamada requerir el uso
de HTTP/2 o proporcionar acceso a marcos HTTP/2 subyacentes.
gRPC Web es una tecnología adicional desde el equipo gRPC que proporciona compatibilidad limitada gRPC en el
explorador. gRPC Web consta de dos partes: un cliente de JavaScript que es compatible con todos los exploradores
modernos y un proxy Web de gRPC en el servidor. El cliente gRPC Web llama al proxy y el servidor proxy
reenviará en las solicitudes de gRPC al servidor gRPC.
No todas las características de gRPC son compatibles con Web gRPC. No se admite el cliente y bidireccionales de
transmisión por secuencias, y hay compatibilidad limitada para el streaming server.
No legible
Las solicitudes de API de HTTP se envían como texto y se pueden leer y creadas por los humanos.
gRPC mensajes se codifican con Protobuf de forma predeterminada. Aunque es eficaz para enviar y recibir
Protobuf, su formato binario no es humano legible. Protobuf requiere la descripción de la interfaz del mensaje
especificado en el *.proto archivo deserializar correctamente. Herramientas adicionales están necesaria para
analizar cargas Protobuf en la conexión y componer las solicitudes a mano.
Características como reflexión server y herramienta de línea de comandos gRPC existe para ayudar a los mensajes
binarios Protobuf. Además, los mensajes de soporte técnico Protobuf conversión a y desde JSON. La conversión
de JSON integrada proporciona una manera eficaz para convertir Protobuf mensajes hacia y desde el formato de
lenguaje natural al depurar.

Escenarios de marco de trabajo alternativo


Se recomiendan otros marcos sobre gRPC en los escenarios siguientes:
Las API de explorador accesible – gRPC no se admite totalmente en el explorador. gRPC Web puede ofrecer
compatibilidad con exploradores, pero tiene limitaciones y presenta a un servidor proxy.
Comunicación en tiempo real de difusión – gRPC admite la comunicación en tiempo real a través de
streaming, pero no existe el concepto de difundir un mensaje de salida para las conexiones registradas. Por
ejemplo en un escenario de salón de chat que deben enviarse los mensajes de chat de nuevo a todos los clientes
en el salón de chat, cada llamada gRPC es necesario transmitir individualmente los nuevos mensajes de chat
para el cliente. SignalR es un marco útil para este escenario. SignalR tiene el concepto de las conexiones
persistentes y la compatibilidad integrada para los mensajes de difusión.
La comunicación entre procesos – un proceso debe hospedar un servidor HTTP/2 para aceptar las llamadas
entrantes gRPC. Para Windows, la comunicación entre procesos canalizaciones es un método rápido y ligero de
comunicación.

Recursos adicionales
Creación de un servidor y un cliente gRPC en ASP.NET Core
Introducción a gRPC en ASP.NET Core
Servicios gRPC con C#
Migrar los servicios gRPC de núcleo de C a ASP.NET Core
Pruebas unitarias páginas de Razor en ASP.NET Core
26/06/2019 • 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: Capa de acceso a
datos (DAL) 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 modelo 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 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 TestDbContextOptions 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.

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


{
// 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á configurada para la prueba o se define el resultado esperado.
2. actuar: Se ejecuta la prueba.
3. Aserción: Las aserciones se realizan para determinar si el resultado de la prueba es correcta.
Por ejemplo, el DeleteMessageAsync método es responsable de quitar un único mensaje identificado por su Id
(src/RazorPagesTestSample/Data/AppDbContext.cs):
public async virtual Task DeleteMessageAsync(int id)
{
var message = await Messages.FindAsync(id);

if (message != null)
{
Messages.Remove(message);
await SaveChangesAsync();
}
}

Hay dos pruebas para este método. Una prueba comprueba que el método elimina un mensaje cuando el mensaje
está presente en la base de datos. Las 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(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}

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.

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.
MÉTODO DEL MODELO DE PÁGINA FUNCIÓN

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
10/05/2019 • 19 minutes to read • Edit Online

Por Steve Smith


Los controles desempeñan un rol fundamental en cualquier aplicación de ASP.NET Core MVC. Por tanto, debe
tener la seguridad de que los controladores se comportan según lo previsto. Las pruebas automatizadas pueden
detectar errores antes de que la aplicación se implemente en un entorno de producción.
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í.
Configure pruebas unitarias de las acciones del controlador para centrarse en el comportamiento del controlador.
Una prueba unitaria del controlador evita escenarios como filtros, enrutamiento y enlace de modelos. Las
pruebas que abarcan las interacciones entre los componentes que colectivamente responden a una solicitud se
controlan mediante pruebas de integración. Para más información sobre las pruebas de integración, vea Pruebas
de integración en ASP.NET Core.
Si va a escribir filtros personalizados y rutas, realice pruebas unitarias en ellos de forma aislada, no como parte
de las pruebas de una acción de controlador concreta.
Para demostrar las pruebas unitarias del controlador, revise el siguiente controlador en la aplicación de ejemplo.
El controlador Home muestra una lista de sesiones de lluvia de ideas y permite crear nuevas sesiones de lluvia de
ideas con una solicitud POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;

public HomeController(IBrainstormSessionRepository sessionRepository)


{
_sessionRepository = sessionRepository;
}

public async Task<IActionResult> Index()


{
var sessionList = await _sessionRepository.ListAsync();

var model = sessionList.Select(session => new StormSessionViewModel()


{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});

return View(model);
}

public class NewSessionModel


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

[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}

return RedirectToAction(actionName: nameof(Index));


}
}

El controlador anterior:
Sigue el Principio de dependencias explícitas.
Espera la inserción de dependencias (DI) para ofrecer una instancia de IBrainstormSessionRepository .
Se puede probar con un servicio IBrainstormSessionRepository ficticio con el uso de un marco de objeto
ficticio, como Moq. Un objeto ficticio es un objeto fabricado con un conjunto predeterminado de
comportamientos de propiedades y métodos utilizados para las pruebas. Para más información, vea
Introducción a las pruebas de integración.
El método HTTP GET Index no tiene bucles ni bifurcaciones y solamente llama a un método. La prueba unitaria
para esta acción:
Realice el simulacro del servicio IBrainstormSessionRepository mediante el método GetTestSessions .
GetTestSessions crea dos sesiones de lluvia de ideas ficticias con fechas y nombres de sesión.
Ejecuta el método Index .
Realiza aserciones sobre el resultado devuelto por el método:
Se devuelve ViewResult.
ViewDataDictionary.Model es StormSessionViewModel .
Hay dos sesiones de lluvia de ideas almacenadas en ViewDataDictionary.Model .

[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);
Assert.Equal(2, model.Count());
}

private List<BrainstormSession> GetTestSessions()


{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}

Las pruebas del método HTTP POST Index del controlador Home verifican que:
Si ModelState.IsValid es false , el método de acción devuelve ViewResult de 400 Solicitud incorrecta con los
datos apropiados.
Cuando ModelState.IsValid es true :
Se llama al método Add del repositorio.
Se devuelve RedirectToActionResult con los argumentos correctos.
Un estado de modelo no válido se comprueba mediante la adición de errores con AddModelError, como se
muestra en la primera prueba de abajo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.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
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}

Si ModelState no es válido, se devuelve el mismo elemento ViewResult que para una solicitud GET. La prueba
no intenta pasar un modelo no válido. Pasar un modelo no válido no es un enfoque válido, ya que el enlace de
modelos no está en ejecución (aunque una prueba de integración usa el enlace de modelos). En este caso, no se
comprueba el enlace de modelos. Estas pruebas unitarias solo comprueban el código del método de acción.
La segunda prueba verifica que, cuando ModelState es válido:
Se agrega un nuevo elemento BrainstormSession (mediante el repositorio).
El método devuelve un elemento RedirectToActionResult con las propiedades esperadas.

Las llamadas ficticias que no se efectúan se suelen ignorar, aunque llamar a Verifiable al final de la llamada de
configuración permite realizar una validación ficticia de la prueba. Esto se realiza 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 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.

SessionController en la aplicación de ejemplo muestra información relacionada con una sesión de lluvia de ideas
determinada. El controlador incluye lógica para tratar valores id no válidos (hay dos escenarios return en el
ejemplo siguiente para abarcar estos escenarios). La última instrucción return devuelve un
StormSessionViewModel nuevo a la vista ( Controllers/SessionController.cs):

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

Las pruebas unitarias incluyen una prueba de cada escenario return en la acción Index del controlador de
sesión:
[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);
}

Al pasar al controlador de ideas, la aplicación expone la funcionalidad como una API web en la ruta api/ideas :
El método ForSession devuelve una lista de ideas ( IdeaDTO ) asociadas con una sesión de lluvia de ideas.
El método Create agrega nuevas ideas a una sesión.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}

var result = session.Ideas.Select(idea => new IdeaDTO()


{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();

return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var session = await _sessionRepository.GetByIdAsync(model.SessionId);


if (session == null)
{
return NotFound(model.SessionId);
}

var idea = new Idea()


{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);

await _sessionRepository.UpdateAsync(session);

return Ok(session);
}

Evite devolver entidades de dominio de negocio directamente mediante llamadas API. Entidades de dominio:
Suelen incluir más datos de los que necesita el cliente.
Innecesariamente acoplan el modelo de dominio interno de la aplicación con la API expuesta públicamente.
La asignación entre las entidades de dominio y los tipos devueltos al cliente se puede realizar:
Manualmente, con una operación Select de LINQ, como la aplicación de ejemplo usa. Para más información,
vea LINQ (Language Integrated Query).
Automáticamente, con una biblioteca, como AutoMapper.
A continuación, la aplicación de ejemplo muestra las pruebas unitarias para los métodos de API Create y
ForSession del controlador de ideas.

La aplicación de ejemplo contiene dos pruebas ForSession . La primera prueba determina si ForSession
devuelve NotFoundObjectResult (HTTP no encontrado) para una sesión no válida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSession(testSessionId);

// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}

La segunda prueba ForSession determina si ForSession devuelve una lista de ideas de sesión ( <List<IdeaDTO>> )
para una sesión válida. Las comprobaciones también examinan la primera idea para confirmar si su propiedad
Name es correcta:

[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

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

Para comprobar el comportamiento del método Create cuando ModelState no es válido, la aplicación de
ejemplo agrega 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 del método de
acción al confrontarlo con un valor de ModelState no válido:

[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);
}
La segunda prueba de Create depende de que el repositorio devuelva null , por lo que el repositorio ficticio
está configurado para devolver 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. La prueba puede realizarse en una sola
instrucción, como se muestra en el código de ejemplo:

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
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);
}

La tercera prueba de Create , Create_ReturnsNewlyCreatedIdeaForSession , verifica que se llama al método


UpdateAsync del repositorio. Se llama al objeto ficticio con Verifiable y, después, se llama al método Verify del
repositorio ficticio para confirmar que se ejecutó el método Verifiable. La prueba unitaria no es responsable de
garantizar que el método UpdateAsync guarda los datos; esto se puede realizar con una prueba de integración.

[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();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Probar ActionResult<T>
En ASP.NET Core 2.1 o posterior, ActionResult<T> (ActionResult<TValue>) permite devolver un tipo que se
deriva de ActionResult o bien un tipo específico.
La aplicación de ejemplo incluye un método que devuelve un resultado List<IdeaDTO> para una sesión
determinada id . Si la sesión id no existe, el controlador devuelve NotFound:

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

Dos pruebas del controlador ForSessionActionResult se incluyen en ApiIdeasControllerTests .


La primera prueba confirma que el controlador devuelve ActionResult , pero no una lista inexistente de ideas
para una sesión inexistente id :
El tipo ActionResult es ActionResult<List<IdeaDTO>> .
Result es NotFoundObjectResult.

[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;

// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

Para obtener un elemento id de sesión válido, la segunda prueba confirma que el método devuelve:
Un ActionResult con un tipo List<IdeaDTO> .
ActionResult<T>.Value es de un tipo List<IdeaDTO> .
El primer elemento de la lista es una idea válida que coincide con la idea almacenada en la sesión ficticia
(obtenido mediante una llamada a GetTestSession ).

[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);

// Act
var result = await controller.ForSessionActionResult(testSessionId);

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}

La aplicación de ejemplo también incluye un método para crear un nuevo elemento Idea para una sesión
determinada. El controlador devuelve:
BadRequest para un modelo no válido.
NotFound si no existe la sesión.
CreatedAtAction cuando se actualiza la sesión con la idea nueva.

[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()


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


}

Tres pruebas de CreateActionResult se incluyen en ApiIdeasControllerTests .


El primer texto confirma que se devuelve BadRequest para un modelo no válido.
[Fact]
public async Task CreateActionResult_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.CreateActionResult(model: null);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

La segunda prueba comprueba que se devuelve NotFound si la sesión no existe.

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);

var newIdea = new NewIdeaModel()


{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};

// Act
var result = await controller.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

Para una sesión válida id , la prueba final confirma que:


El método devuelve ActionResult con un tipo BrainstormSession .
ActionResult<T>.Result es CreatedAtActionResult. CreatedAtActionResult es similar a una respuesta 201
Creado con un encabezado Location .
ActionResult<T>.Value es un tipo BrainstormSession .
La llamada ficticia para actualizar la sesión, UpdateAsync(testSession) , se ha invocado. La llamada al método
Verifiable se comprueba mediante la ejecución de mockRepo.Verify() en las aserciones.
Se devuelven dos objetos Idea para la sesión.
El último elemento (el elemento Idea agregado por la llamada ficticia a UpdateAsync ) coincide con el
elemento newIdea agregado a la sesión en la prueba.
[Fact]
public async Task CreateActionResult_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.CreateActionResult(newIdea);

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

Recursos adicionales
Pruebas de integración en ASP.NET Core
Cree y ejecute pruebas unitarias con Visual Studio.
Pruebas de integración en ASP.NET Core
06/06/2019 • 35 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 espera 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 directory.
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 permite garantizar que los componentes de infraestructura de pruebas accidentalmente no
están incluidos 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

Entorno SUT
Si el SUT entorno no está configurado, los valores predeterminados de entorno para el desarrollo.

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

De forma predeterminada, no se conservan las cookies no esenciales entre solicitudes cuando el directiva de
consentimiento de RGPD está habilitado. Para conservar las cookies no esenciales, tales como las usadas por el
proveedor TempData, marcarlos como esenciales en las pruebas. Para obtener instrucciones sobre cómo
marcar una cookie como esenciales, consulte cookies esencial.
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_2)
.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<TStartup> where TStartup: class
{
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 un directorio diferente que el directorio 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 con JSON.
Agregar el xunit.runner.json archivo a la raíz del proyecto de prueba con el siguiente contenido:

{
"shadowCopy": false
}

Eliminación de objetos
Después de las pruebas de la IClassFixture implementación se ejecutan, TestServer y HttpClient se eliminan
cuando se desecha xUnit el WebApplicationFactory . Si crea una instancia por el desarrollador de objetos
requieren la eliminación, deshacerse de ellos en el IClassFixture implementación. Para obtener más
información, consulte implementar un método Dispose.

Ejemplo de pruebas de integración


El aplicación de ejemplo se compone de dos aplicaciones:

APLICACIÓN DIRECTORIO DEL PROYECTO DESCRIPCIÓN


APLICACIÓN DIRECTORIO 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 directorio:

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 modelo 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 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 directory.

DIRECTORIO DE 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.
DIRECTORIO DE APLICACIÓN DE PRUEBA DESCRIPCIÓN

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
Las pruebas de carga o esfuerzo de ASP.NET Core
10/05/2019 • 5 minutes to read • Edit Online

Pruebas de carga y pruebas de esfuerzo son importantes para asegurarse de que una aplicación web es eficaz y
escalable. Sus objetivos son diferentes, aunque a menudo comparten pruebas similares.
Las pruebas de carga – comprobar si la aplicación puede controlar una carga de usuarios para un determinado
escenario especificado sin dejar de satisfacer el objetivo de respuesta. La aplicación se ejecuta en condiciones
normales.
Las pruebas de esfuerzo – probar la estabilidad de la aplicación cuando se ejecuta en condiciones extremas, con
frecuencia durante un largo período de tiempo. Las pruebas colocan gran carga de usuarios, los picos o
incrementar gradualmente la carga, en la aplicación o limitan los recursos informáticos de la aplicación.
Las pruebas de esfuerzo determinan si una aplicación en situaciones de estrés puede recuperarse de errores y
volver correctamente al comportamiento esperado. En situaciones de estrés, la aplicación no se ejecuta en
condiciones normales.
2019 de Visual Studio es la última versión de Visual Studio con las funciones de prueba de carga. Para los clientes
que requieren en el futuro de las herramientas de prueba de carga, se recomienda herramientas alternativas, como
Apache JMeter, Akamai CloudTest y BlazeMeter. Para obtener más información, consulte el notas de la versión de
Visual Studio 2019.
Finaliza la prueba de carga service en Azure DevOps en 2020. Para obtener más información, consulte prueba final
del servicio del ciclo de vida de carga basado en la nube.

Visual Studio Tools


Visual Studio permite a los usuarios crear, desarrollar y depurar las pruebas de carga y rendimiento web. Una
opción está disponible para crear las pruebas mediante la grabación de acciones en un explorador web.
Para obtener información sobre cómo crear, configurar y ejecutar una prueba de carga de proyectos con Visual
Studio 2017, consulte inicio rápido: Creación de un proyecto de prueba de carga. Para obtener más información,
consulte el recursos adicionales sección.
Las pruebas de carga pueden configurarse para ejecutarse de forma local o en ejecución en la nube con Azure
DevOps.

Azure DevOps
Se pueden iniciar ejecuciones de pruebas de carga con el planes de prueba de Azure DevOps service.
El servicio admite los siguientes formatos de prueba:
Visual Studio – prueba Web creado en Visual Studio.
Archivo HTTP – tráfico HTTP capturado dentro del archivo se reproduce durante las pruebas.
Basado en URL – permite especificar direcciones URL para la carga de prueba, tipos de solicitud, los
encabezados y las cadenas de consulta. Ejecutar configuración de los parámetros como la duración, modelo de
carga y el número de usuarios pueden configurarse.
Apache JMeter.

Azure Portal
Portal de Azure permite configurar y ejecutar las pruebas de carga de aplicaciones web directamente desde el
rendimiento ficha de App Service en Azure portal.

La prueba puede ser una prueba manual con una dirección URL especificada o un archivo de prueba Web de Visual
Studio, que puede probar varias direcciones URL.

Al final de la prueba, informes generados muestran las características de rendimiento de la aplicación. Estadísticas
de ejemplo incluyen:
Tiempo medio de respuesta
Rendimiento máximo: solicitudes por segundo
Porcentaje de errores

Herramientas de terceros
En la lista siguiente contiene las herramientas de rendimiento web de terceros con varios conjuntos de
características:
Apache JMeter
ApacheBench (ab)
Gatling
Algarrobas
West Wind WebSurge
Netling

Recursos adicionales
Serie de blogs de prueba de carga
Solución de problemas de proyectos de ASP.NET
Core
24/06/2019 • 7 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): Diagnosticar 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 32 bits y 64 bits del SDK de .NET Core
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 bits 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 muestran.

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

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.
Desinstale todos los 32-bit SDK de .NET Core y los tiempos de ejecución 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 Visual Studio 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 .

Cuando se ejecuta un dotnet comando, aparece la advertencia como:

No era posible encontrar cualquier SDK de dotnet instalado.

Estas advertencias aparecen cuando la variable de entorno PATH no apunta a ningún SDK de .NET Core en el
equipo. Para resolver este problema:
Instale el SDK de .NET Core. Obtener el instalador más reciente de descargas de .NET.
Compruebe que la PATH variable de entorno se apunta a la ubicación donde está instalado el SDK (
C:\Program Files\dotnet\ para 64 -bit/x64 o C:\Program Files (x86)\dotnet\ para 32 -bit/x86 ). El instalador
del SDK se establece normalmente el PATH . Instale siempre el mismo valor de bits SDK y los tiempos de
ejecución en el mismo equipo.
Faltan SDK después de instalar el lote de hospedaje de .NET Core
Instalar el conjunto de hospedaje de .NET Core modifica el PATH cuando instala el tiempo de ejecución de .NET
Core para que apunte a la versión de 32 bits (x 86) de .NET Core ( C:\Program Files (x86)\dotnet\ ). Esto puede
provocar que faltan SDK cuando el núcleo de .NET de 32 bits (x 86) dotnet se usa el comando (ningún SDK de
.NET Core se detectaron). Para resolver este problema, mueva C:\Program Files\dotnet\ a una posición delante
C:\Program Files (x86)\dotnet\ en el PATH .

Obtención de datos de una aplicación


Si una aplicación es capaz de responder a las solicitudes, puede obtener los siguientes datos de la aplicación con
middleware:
Solicitar – método, esquema, host, pathbase, ruta de acceso, cadena de consulta, encabezados
Conexión – dirección IP remota, puerto remoto, dirección IP local, puerto local, el certificado de cliente
Identidad – nombre, nombre para mostrar
Valores de configuración
Variables de entorno
Incluya las líneas siguientes middleware código al principio de la Startup.Configure canalización de
procesamiento de solicitudes del método. El entorno se comprueba antes de ejecuta el software intermedio para
asegurarse de que el código solo se ejecuta en el entorno de desarrollo.
Para obtener el entorno, use cualquiera de los métodos siguientes:
Insertar el IHostingEnvironment en el Startup.Configure método y comprobar el entorno con la variable
local. El código de ejemplo siguiente muestra este enfoque.
Asignar el entorno a una propiedad en el Startup clase. Comprobar el entorno mediante la propiedad
(por ejemplo, if (Environment.IsDevelopment()) ).

public void Configure(IApplicationBuilder app, IHostingEnvironment env,


IConfiguration config)
{
if (env.IsDevelopment())
{
app.Run(async (context) =>
{
var sb = new StringBuilder();
var nl = System.Environment.NewLine;
var rule = string.Concat(nl, new string('-', 40), nl);
var authSchemeProvider = app.ApplicationServices
.GetRequiredService<IAuthenticationSchemeProvider>();

sb.Append($"Request{rule}");
sb.Append($"{DateTimeOffset.Now}{nl}");
sb.Append($"{context.Request.Method} {context.Request.Path}{nl}");
sb.Append($"Scheme: {context.Request.Scheme}{nl}");
sb.Append($"Host: {context.Request.Headers["Host"]}{nl}");
sb.Append($"PathBase: {context.Request.PathBase.Value}{nl}");
sb.Append($"Path: {context.Request.Path.Value}{nl}");
sb.Append($"Query: {context.Request.QueryString.Value}{nl}{nl}");

sb.Append($"Connection{rule}");
sb.Append($"RemoteIp: {context.Connection.RemoteIpAddress}{nl}");
sb.Append($"RemotePort: {context.Connection.RemotePort}{nl}");
sb.Append($"LocalIp: {context.Connection.LocalIpAddress}{nl}");
sb.Append($"LocalPort: {context.Connection.LocalPort}{nl}");
sb.Append($"ClientCert: {context.Connection.ClientCertificate}{nl}{nl}");

sb.Append($"Identity{rule}");
sb.Append($"User: {context.User.Identity.Name}{nl}");
var scheme = await authSchemeProvider
.GetSchemeAsync(IISDefaults.AuthenticationScheme);
sb.Append($"DisplayName: {scheme?.DisplayName}{nl}{nl}");

sb.Append($"Headers{rule}");
foreach (var header in context.Request.Headers)
{
sb.Append($"{header.Key}: {header.Value}{nl}");
}
sb.Append(nl);

sb.Append($"Websockets{rule}");
if (context.Features.Get<IHttpUpgradeFeature>() != null)
{
sb.Append($"Status: Enabled{nl}{nl}");
}
else
{
sb.Append($"Status: Disabled{nl}{nl}");
}

sb.Append($"Configuration{rule}");
foreach (var pair in config.AsEnumerable())
{
sb.Append($"{pair.Path}: {pair.Value}{nl}");
}
sb.Append(nl);

sb.Append($"Environment Variables{rule}");
var vars = System.Environment.GetEnvironmentVariables();
foreach (var key in vars.Keys.Cast<string>().OrderBy(key => key,
StringComparer.OrdinalIgnoreCase))
{
var value = vars[key];
sb.Append($"{key}: {value}{nl}");
}

context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(sb.ToString());
});
}
}
Registro en ASP.NET Core
05/07/2019 • 50 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 gran variedad de proveedores
de registro integrados y de terceros. En este artículo se muestra cómo usar las API de registro con
proveedores integrados.
Vea o descargue el código de ejemplo (cómo descargarlo)

Incorporación de proveedores
Un proveedor de registro muestra o almacena registros. Por ejemplo, el proveedor de consola muestra los
registros en la consola y el proveedor de Azure Application Insights los almacena en Azure Application
Insights. Los registros se pueden enviar a varios destinos mediante la incorporación de varios
proveedores.
Para usar un proveedor, llame al método de extensión Add{provider name} 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) =>
{
// Requires `using Microsoft.Extensions.Logging;`
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.UseStartup<Startup>()
.Build();

webHost.Run();
}

El código anterior requiere referencias a Microsoft.Extensions.Logging y


Microsoft.Extensions.Configuration .

La plantilla de proyecto predeterminada llama a CreateDefaultBuilder, que agrega los siguientes


proveedores de registro:
Consola
Depuración
EventSource (a partir de ASP.NET Core 2.2)
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

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


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

Si usa CreateDefaultBuilder , puede reemplazar los proveedores predeterminados por sus propios valores.
Llame a ClearProviders y agregue los proveedores que desee.

public static void Main(string[] args)


{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});

Para usar un proveedor, instale su paquete NuGet y llame al método de extensión del proveedor en una
instancia de ILoggerFactory:

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 agrega proveedores de registro en el método Startup.Configure . Para 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 los proveedores de registro de
terceros más adelante en el artículo.

Creación de registros
Obtenga un objeto ILogger<TCategoryName> a partir de la inserción de dependencias.
El siguiente ejemplo de controlador crea los registros Information y Warning . La categoría es
TodoApiSample.Controllers.TodoController (el nombre de clase completo de TodoController en la
aplicación de ejemplo):

public class TodoController : Controller


{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

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 siguiente ejemplo de Razor Pages crea registros con Information como el nivel y
TodoApiSample.Pages.AboutModel como la categoría:

public class AboutModel : PageModel


{
private readonly ILogger _logger;

public AboutModel(ILogger<AboutModel> logger)


{
_logger = logger;
}

public void OnGet()


{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation("Message displayed: {Message}", Message);
}
public class TodoController : Controller
{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;

public TodoController(ITodoRepository todoRepository,


ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

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 ejemplo anterior crea registros con Information y Warning como el nivel y la clase TodoController
como la categoría.
El nivel de registro indica la gravedad del evento registrado. La categoría de registro es una cadena que
está asociada con cada registro. La ILogger<T> instancia crea registros que tienen el nombre completo del
tipo T como la categoría. Los niveles y las categorías se explican detalladamente más adelante en este
artículo.
Creación de registros durante el inicio
Para escribir registros en la clase Startup , incluya un parámetro ILogger en la signatura de construcción:
public class Startup
{
private readonly ILogger _logger;

public Startup(IConfiguration configuration, ILogger<Startup> logger)


{
Configuration = configuration;
_logger = logger;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

// Add our repository type


services.AddSingleton<ITodoRepository, TodoRepository>();
_logger.LogInformation("Added TodoRepository to services");
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
if (env.IsDevelopment())
{
_logger.LogInformation("In Development environment");
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseMvc();
}
}

Creación de registros en la clase Program


Para escribir registros la clase Program , obtenga una instancia ILogger de inserción de dependencias:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
});

No hay métodos de registrador asincrónicos


El registro debe ser tan rápido que no merezca la pena el costo de rendimiento del código asincrónico. Si
el almacén de datos de registro es lento, no escriba directamente en él. Considere la posibilidad de escribir
los mensajes de registro en un almacén rápido inicialmente y luego moverlos a la tienda lenta. Por
ejemplo, si inicia sesión en SQL Server, no desea hacerlo directamente en un método Log , ya que los
métodos Log son sincrónicos. En su lugar, agregue sincrónicamente mensajes de registro a una cola en
memoria y haga que un trabajo en segundo plano extraiga los mensajes de la cola para realizar el trabajo
asincrónico de insertar datos en SQL Server.

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

La propiedad Logging puede tener LogLevel y propiedades del proveedor de registro (se muestra la
consola).
La propiedad LogLevel bajo Logging especifica el nivel mínimo que se va a registrar para las categorías
seleccionadas. En el ejemplo, las categorías System y Microsoft se registran en el nivel Information y
todas las demás se registran en el nivel Debug .
Otras propiedades bajo Logging especifican proveedores de registro. El ejemplo es para el proveedor de
consola. Si un proveedor admite ámbitos de registro, IncludeScopes indica si están habilitados. Una
propiedad de proveedor (como Console en el ejemplo) también puede especificar una propiedad
LogLevel . LogLevel en un proveedor especifica niveles de registro para ese proveedor.

Si los niveles se especifican en Logging.{providername}.LogLevel , invalidan todo lo establecido en


Logging.LogLevel .

{
"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, los registros aparecen en la consola
cuando la aplicación se ejecuta 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

Los registros anteriores se generaron mediante la realización de una solicitud HTTP GET a la aplicación de
ejemplo en http://localhost:5000/api/todo/0 .
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 del
código de marco de ASP.NET Core. 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
Cuando se crea un objeto ILogger , se ha especificado una categoría para él. Esa categoría se incluye con
cada mensaje de registro creado por esa instancia de ILogger . La categoría puede ser cualquier cadena,
pero la convención es usar el nombre de clase, como "TodoApi.Controllers.TodoController".
Use ILogger<T> para obtener una instancia ILogger que utiliza el nombre de tipo completo de T como
la categoría:
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;
}

Para especificar explícitamente la categoría, llame a ILoggerFactory.CreateLogger :

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

ILogger<T> es equivale a llamar a CreateLogger con el nombre de tipo completo de T .

Nivel de registro
Cara registro especifica un valor LogLevel. El nivel de registro indica la gravedad o importancia. Por
ejemplo, podría escribir un registro Information cuando un método termina con normalidad y un registro
Warning cuando un método devuelve un código de estado 404 No encontrado.

El siguiente código crea los registros Information y Warning :


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 el código anterior, el primer parámetro es el id. de 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 en la sección de la
plantilla de mensaje más adelante en este artículo.
Los métodos de registro que incluyen el nivel en el nombre del método (por ejemplo LogInformation y
LogWarning ) son métodos de extensión para ILogger. 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 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 información que normalmente solo es útil para la depuración. 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.
Depurar = 1
Para información que puede ser útil para el desarrollo y la depuración. Ejemplo:
Entering method Configure with flag set to true. Habilite los registros de nivel Debug en
producción cuando 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.
Use 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, envíe Trace a través del nivel Information a un almacén de datos de volumen. Envíe
Warning a través de Critical a un almacén de datos de valor.
Durante el desarrollo, envíe Warning a través de Critical a la consola y agregue Trace a través de
Information cuando solucione problemas.

En la sección Filtrado del registro de este artículo se explica cómo controlar los niveles de registro que
controla un proveedor.
ASP.NET Core escribe registros de eventos de marco. En los ejemplos de registro anteriores de este
artículo se excluyeron los registros por debajo del nivel Information , por lo que no se crearon los registros
de nivel Debug o Trace . Este es un ejemplo de registros de consola generados mediante la ejecución de la
aplicación de ejemplo configurada para mostrar registros Debug :

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 registro se 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 id. de evento asocia un conjunto de eventos. Por ejemplo, todos los registros relacionados con la
presentación de una lista de elementos en una página podrían ser 1001.
El proveedor de registro puede almacenar el id. de evento en un campo de identificador, en el mensaje de
registro o no almacenarlo. El proveedor de depuración no muestra los identificadores de evento. El
proveedor de consola muestra los identificadores de evento entre corchetes 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 registro especifica una plantilla de mensaje. La plantilla de mensaje puede contener marcadores de
posición para los que se proporcionan argumentos. Use los nombres de los marcadores de posición, no
números.

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. En el código siguiente, tenga en cuenta que los nombres de parámetro están
fuera de la secuencia en la plantilla de mensaje:

string p1 = "parm1";
string p2 = "parm2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);

Este código crea un mensaje de registro con los valores de parámetro en secuencia:

Parameter values: parm1, parm2

La plataforma de registro funciona de esta manera para que los proveedores de registro puedan
implementar el registro semántico, también conocido como registro estructurado. Los propios argumentos
se pasan al sistema de registro, no solo a la plantilla de mensaje con formato. Esta información permite a
los proveedores de registro almacenar los valores de parámetro como campos. Por ejemplo, suponga que
las llamadas del método del registrador tiene el aspecto siguiente:
_logger.LogInformation("Getting item {ID} at {RequestTime}", id, DateTime.Now);

Si envía los registros a Azure Table Storage, cada entidad de Azure Table puede tener propiedades ID y
RequestTime , lo que simplifica las consultas en los datos de registro. Una consulta puede buscar todos los
registros dentro de un intervalo RequestTime determinado sin 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.
Para suprimir todos los registros, especifique 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
El código de la plantilla de proyecto 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) =>
{
// Requires `using Microsoft.Extensions.Logging;`
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
})
.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 para todos los proveedores. Se elige una sola regla para cada proveedor cuando se
crea un objeto ILogger .
Reglas de filtro en el código
En el siguiente ejemplo se muestra cómo registrar reglas de filtro en el código:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace));

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.Mv Advertencia


c.Razor.Internal

3 Consola Microsoft.AspNetCore.Mv Depuración


c.Razor.Razor

4 Consola Microsoft.AspNetCore.Mv Error


c.Razor

5 Consola Todas las categorías Información

6 Todos los proveedores Todas las categorías Depuración

7 Todos los proveedores Sistema Depuración

8 Depuración Microsoft Seguimiento

Cuando se crea un objeto ILogger , el objeto ILoggerFactory selecciona una sola regla por proveedor para
aplicar a ese registrador. Todos los mensajes escritos por una instancia 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
coincidencia, 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 coincidencia, 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 .
Con la lista de reglas anterior, supongamos que 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.
La instancia ILogger resultante envía los registros de nivel Trace y superiores al proveedor de
depuración. Los registros de nivel Debug y superiores se envían al proveedor de consola.
Alias de proveedor
Cada proveedor define un alias que se puede utilizar en la configuración en lugar del nombre de tipo
completo. Para los proveedores integrados, use los alias siguientes:
Consola
Depuración
EventSource
EventLog
TraceSource
AzureAppServicesFile
AzureAppServicesBlob
ApplicationInsights
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));

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
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, la categoría y el
nivel de registro. 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;
});
});

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

Para establecer reglas de filtrado para todos los proveedores que están registrados con un instancia de
ILoggerFactory , use 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 los
registros creados por el código de aplicación se registran 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();

Para evitar que los registros se escriban, especifique LogLevel.None como el nivel de registro mínimo. 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.

Niveles y categorías del sistema


Estas son algunas categorías que ASP.NET Core y Entity Framework Core usan, con notas sobre lo que los
registros de espera de ellas:

CATEGORÍA NOTAS

Microsoft.AspNetCore Diagnósticos generales de ASP.NET Core.

Microsoft.AspNetCore.DataProtection Qué claves se tuvieron en cuenta, encontraron y usaron.


CATEGORÍA NOTAS

Microsoft.AspNetCore.HostFiltering Hosts permitidos.

Microsoft.AspNetCore.Hosting Cuánto tiempo tardaron en completarse las solicitudes


HTTP y a qué hora comenzaron. Qué ensamblados de
inicio de hospedaje se cargaron.

Microsoft.AspNetCore.Mvc Diagnósticos de MVC y Razor. Enlace de modelos,


ejecución de filtros, compilación de vistas y selección de
acciones.

Microsoft.AspNetCore.Routing Información de coincidencia de ruta.

Microsoft.AspNetCore.Server Inicio y detención de conexión y mantener las respuestas


activas. Información de certificado HTTPS.

Microsoft.AspNetCore.StaticFiles Archivos servidos.

Microsoft.EntityFrameworkCore Diagnósticos generales de Entity Framework Core.


Actividad y la configuración de bases de datos, detección
de cambios y migraciones.

Ámbitos de registro
Un ámbito puede agrupar un conjunto de operaciones lógicas. Esta agrupación se puede utilizar para
adjuntar los mismos datos para cada registro que se crea como parte de un conjunto. Por ejemplo, cada
registro creado como parte del procesamiento de una transacción puede incluir el identificador de dicha
transacción.
Un ámbito es un tipo IDisposable devuelto por el método BeginScope y se conserva hasta que se elimina.
Use un ámbito encapsulando las llamadas de registrador en un bloque using :

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
AzureAppServicesFile
AzureAppServicesBlob
ApplicationInsights
Para información sobre el registro de stdout, consulte Solución de problemas de ASP.NET Core en IIS y
Solución de problemas de ASP.NET Core en 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.

El proveedor de consola tiene un impacto importante en el rendimiento y no suele ser adecuado para su
uso en producción.
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.
Para ver una salida de registro de la consola, abra un símbolo del sistema en la carpeta del proyecto y
ejecute este comando:

dotnet run

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
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 puede 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.
El paquete de proveedor no está incluido en el metapaquete Microsoft.AspNetCore.App. Para usar el
proveedor, instale el paquete.
Si el destino es .NET Framework o hace referencia al metapaquete Microsoft.AspNetCore.App , agregue el
paquete de proveedor al proyecto. Invoke AddAzureWebAppDiagnostics :
logging.AddAzureWebAppDiagnostics();

loggerFactory.AddAzureWebAppDiagnostics();

Una sobrecarga AddAzureWebAppDiagnostics permite pasar AzureAppServicesDiagnosticsSettings. El


objeto de configuración puede invalidar la configuración predeterminada, como la plantilla de salida de
registro, el nombre de blob y el límite de tamaño de archivo. (La plantilla salida es una plantilla de mensaje
que se aplica a todos los registros además de que se proporciona con una llamada de método ILogger ).
Para configurar las opciones de proveedor, use AzureFileLoggerOptions y AzureBlobLoggerOptions, tal y
como se muestra en el ejemplo siguiente:

public static void Main(string[] args)


{
var host = CreateWebHostBuilder(args).Build();

var todoRepository = host.Services.GetRequiredService<ITodoRepository>();


todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });

var logger = host.Services.GetRequiredService<ILogger<Program>>();


logger.LogInformation("Seeded the database.");

host.Run();
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddAzureWebAppDiagnostics())
.ConfigureServices(serviceCollection => serviceCollection
.Configure<AzureFileLoggerOptions>(options =>
{
options.FileName = "azure-diagnostics-";
options.FileSizeLimit = 50 * 1024;
options.RetainedFileCountLimit = 5;
}).Configure<AzureBlobLoggerOptions>(options =>
{
options.BlobName = "log.txt";
}))
.UseStartup<Startup>();

Al realizar una implementación en una aplicación de App Service, esta respeta la configuración de la
sección Registros de App Service de la página App Service de Azure Portal. Cuando se actualiza la
configuración siguiente, los cambios se aplican de inmediato sin necesidad de reiniciar ni de volver a
implementar la aplicación.
Registro de la aplicación (sistema de archivos)
Registro de la aplicación (blob)
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.
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).
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 App Service desde la página de portal de la aplicación.
Establezca Registro de la aplicación (sistema de archivos) en Activado.
Elija el Nivel de registro.
Navegue hasta la página Secuencia de registro para consultar los mensajes de la aplicación. La
aplicación los registra a través de la interfaz ILogger .
Registro de seguimiento de Azure Application Insights
El proveedor de paquete Microsoft.Extensions.Logging.ApplicationInsights escribe los registros en Azure
Application Insights. Application Insights es un servicio que supervisa una aplicación web y proporciona
herramientas para consultar y analizar los datos de telemetría. Si usa este proveedor, puede consultar y
analizar los registros mediante las herramientas de Application Insights.
El proveedor de registro se incluye como dependencia de Microsoft.ApplicationInsights.AspNetCore, que
es el paquete que proporciona toda la telemetría disponible para ASP.NET Core. Si usa este paquete, no
tiene que instalar el proveedor de paquete.
No use el paquete Microsoft.ApplicationInsights.Web —que es para ASP.NET 4.x.
Para obtener más información, vea los siguientes recursos:
Información general de Application Insights
Application Insights para aplicaciones de ASP.NET Core: comience aquí si quiere implementar la
variedad completa de telemetría de Application Insights junto con el registro.
ApplicationInsightsLoggerProvider para los registros de .NET Core ILogger: comience aquí si quiere
implementar el proveedor de registro sin el resto de la telemetría de Application Insights.
Adaptadores de registro de Application Insights
Instalación, configuración e inicialización del SDK de Application Insights: tutorial interactivo en el sitio
de Microsoft Learn.

NOTE
A partir del 1 de mayo de 2019, el artículo titulado Application Insights para ASP.NET Core está desactualizado y los
pasos del tutorial no funcionan. Consulte en su lugar Application Insights para aplicaciones de ASP.NET Core. Somos
conscientes del problema y estamos trabajando para corregirlo.

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)
Sentry (repositorio de GitHub)
Serilog (repositorio de GitHub)
Stackdriver (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 ILoggerFactory .

Para más información, vea la documentación de cada proveedor. Microsoft no admite los proveedores de
registro de terceros.

Recursos adicionales
Registro de alto rendimiento con LoggerMessage en ASP.NET Core
Páginas de Razor de ASP.NET Core con EF Core:
serie de tutoriales
10/05/2019 • 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
17/05/2019 • 29 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


En la aplicación web de ejemplo Contoso University se muestra cómo crear 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/AspNetCore.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 scaffold.
En el cuadro de diálogo Agregar scaffold, seleccione Páginas de Razor 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.Models
{
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();
}
}
}

Nota: El código anterior usa Models para el espacio de nombres ( namespace ContosoUniversity.Models )
en lugar de Data . Models es coherente con el código generado por el proveedor de scaffolding. Para
obtener más información, consulte este problema de scaffolding de GitHub.
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


El nombre de la base de datos se genera a partir del nombre de contexto proporcionado anteriormente,
más un guión y un GUID. Por lo tanto, el nombre de la base de datos será "SchoolContext-{GUID }". El
GUID será diferente para cada usuario. Abra el Explorador de objetos de SQL Server (SSOX) desde
el menú Vista en Visual Studio. En SSOX, haga clic en (localdb)\MSSQLLocalDB > Databases >
SchoolContext-{GUID }.
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.

Recursos adicionales
Versión en YouTube de este tutorial

S IG U IE N T E
Páginas de Razor con EF Core en ASP.NET Core:
CRUD (2 de 8)
17/06/2019 • 21 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF Core
y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se revisa y personaliza el código CRUD (crear, leer, actualizar y eliminar) con scaffolding.
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 repositorio de servicio para crear una
capa de abstracción entre la interfaz de usuario (las páginas de Razor) y la capa de acceso a datos.
En este tutorial se examinan las páginas Create, Edit, Delete y Details de Razor Pages de la carpeta Students.
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 Asistente 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
Students/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
Students/Index no debe contener una plantilla de ruta:

@page "{id:int}"

Cada página de Razor debe incluir la directiva @page .

Recursos adicionales
Versión en YouTube de este tutorial

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)
10/05/2019 • 25 minutes to read • Edit Online

Por Tom Dykstra, Rick Anderson y Jon P Smith


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF
Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial se agregan las funcionalidades de ordenación, filtrado, agrupación y paginación.
En la siguiente ilustración se muestra una página completa. Los encabezados de columna son vínculos
interactivos para ordenar la columna. Si se hace clic de forma consecutiva en el encabezado de una columna, el
criterio de ordenación cambia entre ascendente y descendente.

Si experimenta problemas que no puede resolver, descargue la aplicación completada.

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


Las plantillas web de ASP.NET Core 2.2 no incluyen la página About. Si usa ASP.NET Core 2.2, cree la página
About de Razor Pages.
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;

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
Versión en YouTube de este tutorial
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)
10/05/2019 • 10 minutes to read • Edit Online

Por Tom Dykstra, Jon P Smith y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF
Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En este tutorial, se usa la característica de migraciones de EF Core para administrar cambios en el modelo de
datos.
Si experimenta problemas que no puede resolver, descargue la aplicación completada.
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 EnsureCreated :

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
Versión en YouTube de este tutorial
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)
17/05/2019 • 49 minutes to read • Edit Online

Por Tom Dykstra y Rick Anderson


La aplicación web Contoso University muestra cómo crear aplicaciones web de las páginas de Razor con EF
Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.
En los tutoriales anteriores se trabajaba con un modelo de datos básico que se componía de tres entidades. En
este tutorial:
Se agregan más entidades y relaciones.
Se personaliza el modelo de datos especificando el formato, la validación y las reglas de asignación de la base
de datos.
Las clases de entidad para el modelo de datos completo se muestran en la siguiente ilustración:
Si experimenta problemas que no puede resolver, descargue la aplicación completada.

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 del asistente de etiquetas <entrada>.
Ejecutar 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 de las claves externas que no acepten valores
NULL ni 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 la propiedad Department.InstructorID no se ha definido como que acepta valores NULL:
EF Core configura una regla de eliminación en cascada para eliminar el departamento cuando se elimina
el instructor.
Eliminar el departamento cuando se elimine el instructor no es el comportamiento previsto.
La API fluida siguiente establecería una regla de restricción en lugar de en cascada.

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 con 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. Consulte
Enrollments y CourseAssignments para obtener ejemplos de cómo pueden inicializarse las tablas de
combinación de varios a varios.

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'.
Aplicar la migración
Ahora que tiene una base de datos existente, debe pensar cómo aplicar los cambios futuros en ella. En este
tutorial se muestran dos enfoques:
Quitar y volver a crear la base de datos
Aplicar la migración a la base de datos existente. Aunque este método es más complejo y lento, es el método
preferido para entornos de producción del mundo real. Nota: Esta sección del tutorial es opcional. Puede
realizar la operación de quitar y volver a crear, y omitir esta sección. Si quiere seguir los pasos descritos en
esta sección, no realice la operación de quitar y volver a crear.
Quitar y volver a crear 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.


Ejecutar 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.
Aplicar la migración a la base de datos existente
Esta sección es opcional. Estos pasos solo funcionan si pasó por alto la sección Quitar y volver a crear la base de
datos.
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"
:

migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);

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.


Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
10/05/2019 • 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 o vea la aplicación completada. Instrucciones de
descarga.
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. Se ha agregado la carga diferida a EF Core en la versión 2.1. 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="Create">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.

Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
10/05/2019 • 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 o vea la aplicación completada. Instrucciones de descarga.
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 Asistente 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 un asistente 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
= instructorToUpdate
.CourseAssignments
.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.
Recursos adicionales
Versión en YouTube de este tutorial (parte 1)
Versión en YouTube de este tutorial (parte 2)

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)
11/06/2019 • 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 o vea la aplicación
completada. Instrucciones de descarga.

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 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 IActionResult HandleDeletedDepartment()


{
var 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 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 publicado 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
Versión de YouTube de este tutorial (gestión de conflictos de simultaneidad)
Versión en YouTube de este tutorial (parte 2)
Versión en YouTube de este tutorial (parte 3)

A N T E R IO R
ASP.NET Core MVC con EF Core: serie de tutoriales
10/05/2019 • 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.
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
Tutorial: Introducción a EF Core en una aplicación
web de ASP.NET Core MVC
21/05/2019 • 40 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.
En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones web de ASP.NET
Core 2.2 MVC con Entity Framework (EF ) Core 2.2 y Visual Studio 2017 o 2019.
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.
En este tutorial ha:
Creación de una aplicación web de ASP.NET Core MVC
Configurar el estilo del sitio
Obtiene información sobre los paquetes NuGet de EF Core
Crear el modelo de datos
Crear el contexto de base de datos
Registrar el contexto para la inserción de dependencias
Inicializar la base de datos con datos de prueba
Crear un controlador y vistas
Consulta la base de datos

Requisitos previos
SDK de .NET Core 2.2
Visual Studio 2019 con las cargas de trabajo siguientes:
Carga de trabajo de ASP.NET y desarrollo web
Carga de trabajo Desarrollo multiplataforma de .NET Core

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.

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.
Creación de una aplicación web
Abra Visual Studio.
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.
Seleccione .NET Core, ASP.NET Core 2.2 y la plantilla Aplicación web (controlador de vista de
modelos).
Asegúrese de que Autenticación esté establecida en Sin autenticación.
Seleccione 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 About, Students, Courses, Instructors y Departments, y elimine la
entrada de menú Privacy.
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 include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/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"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-
shadow mb-3">
shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso
University</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-
collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-
action="About">About</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-
action="Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-
action="Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-
action="Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-
action="Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">


<div class="container">
&copy; 2019 - Contoso University - <a asp-area="" asp-controller="Home" asp-
action="Privacy">Privacy</a>
</div>
</footer>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-
bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

@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/AspNetCore.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.
Acerca de los paquetes NuGet de EF 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.App, por lo que no es necesario hacer referencia al paquete.
Este paquete de SQL Server de EF 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");
}
}
}

Registra SchoolContext
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.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

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;
using Microsoft.AspNetCore.Http;

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

Inicializa 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=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}

El código comprueba si hay estudiantes en la base de datos, 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 = CreateWebHostBuilder(args).Build();

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.

Crea 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.
En el cuadro de diálogo Agregar scaffold:
Seleccione Controlador de MVC con vistas que usan Entity Framework.
Haga clic en Agregar. Aparece el cuadro de diálogo Agregar un controlador de MVC con vistas
que usan Entity Framework.

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
Students 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.
Consulta 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\<su_nombre_de_usuario>.


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 <nombre de la propiedad
de navegación><nombre de la propiedad de clave principal (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 <nombre de la propiedad de clave principal>
(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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Creado una aplicación web de ASP.NET Core MVC
Configurar el estilo del sitio
Obtenido información sobre los paquetes NuGet de EF Core
Creado el modelo de datos
Creado el contexto de la base de datos
Registrado SchoolContext
Inicializado la base de datos con datos de prueba
Creado un controlador y vistas
Consultado la base de datos
En el tutorial siguiente, obtendrá información sobre cómo realizar operaciones CRUD (crear, leer, actualizar y
eliminar) básicas.
Pase al tutorial siguiente para obtener información sobre cómo realizar operaciones CRUD (crear, leer, actualizar
y eliminar) básicas.
Implementación de la funcionalidad CRUD básica
Tutorial: Implementación de la funcionalidad CRUD:
ASP.NET MVC con EF Core
17/06/2019 • 38 minutes to read • Edit Online

En el tutorial anterior, creó una aplicación MVC que almacena y muestra los datos con Entity Framework y SQL
Server LocalDB. En este tutorial, podrá revisar y personalizar el código CRUD (crear, leer, actualizar y eliminar)
que el scaffolding de MVC crea automáticamente para usted en controladores y vistas.

NOTE
Es una práctica habitual implementar el modelo de repositorio con el fin de crear una capa de abstracción entre el
controlador y la capa de acceso a datos. Para que estos tutoriales sean sencillos y se centren en enseñar a usar Entity
Framework, no se usan repositorios. Para obtener información sobre los repositorios con EF, vea el último tutorial de esta
serie.

En este tutorial ha:


Personalizar la página de detalles
Actualizar la página Create
Actualizar la página Edit
Actualizar la página Delete
Cerrar conexiones de bases de datos

Requisitos previos
Introducción a EF Core y ASP.NET Core MVC

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()
.FirstOrDefaultAsync(m => m.ID == id);

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

return View(student);
}

Los métodos Include y ThenInclude hacen que el contexto cargue la propiedad de navegación
Student.Enrollments y, dentro de cada inscripción, la propiedad de navegación Enrollment.Course . Obtendrá
más información sobre estos métodos en el tutorial de lectura de datos relacionados.
El método AsNoTracking mejora el rendimiento en casos en los que no se actualizarán las entidades devueltas
en la duración del contexto actual. Obtendrá más información sobre AsNoTracking al final de este tutorial.
Datos de ruta
El valor de clave que se pasa al método Details procede de los datos de ruta. Los datos de ruta son los que el
enlazador de modelos encuentra en un segmento de la dirección URL. Por ejemplo, la ruta predeterminada
especifica los segmentos de controlador, acción e identificador:

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

En la dirección URL siguiente, la ruta predeterminada asigna Instructor como el controlador, Index como la
acción y 1 como el identificador; estos son los valores de datos de ruta.

http://localhost:1230/Instructor/Index/1?courseID=2021

La última parte de la dirección URL ("?courseID=2021") es un valor de cadena de consulta. El enlazador de


modelos también pasará el valor ID al parámetro id del método Details si se pasa como un valor de cadena
de consulta:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

En la página Index, las instrucciones del asistente 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 los asistentes de etiquetas, vea Asistentes de etiquetas en ASP.NET Core.
Agregar inscripciones a la vista de detalles
Abra Views/Students/Details.cshtml. Cada campo se muestra mediante los asistentes DisplayNameFor y
DisplayFor , como se muestra en el ejemplo siguiente:

<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd class="col-sm-10">
@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 class="col-sm-2">
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
<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 los asistentes 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.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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.FindAsync(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).

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

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

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado la página de detalles
Actualizado la página Create
Actualizado la página Edit
Actualizado la página Delete
Cerrado conexiones de bases de datos
Pase al tutorial siguiente para obtener información sobre cómo expandir la funcionalidad de la página Index
mediante la adición de ordenación, filtrado y paginación.
Siguiente: Ordenación, filtrado y paginación
Tutorial: Adición de ordenación, filtrado y
paginación: ASP.NET MVC con EF Core
17/05/2019 • 25 minutes to read • Edit Online

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.

En este tutorial ha:


Agrega vínculos de ordenación de columnas
Agrega un cuadro de búsqueda
Agrega paginación al índice de Students
Agrega paginación al método Index
Agrega vínculos de paginación
Crea una página About

Requisitos previos
Implementación de la funcionalidad CRUD

Agrega vínculos de ordenación de columnas


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.
Agrega un cuadro de búsqueda
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 llamar al método ToUpper para hacer explícitamente que la prueba no distinga entre
mayúsculas y minúsculas: Where(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 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, 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.

Agrega paginación al í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.

Agrega 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? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

if (searchString != null)
{
pageNumber = 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(), pageNumber ?? 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? pageNumber)

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)
{
pageNumber = 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(), pageNumber ?? 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 (pageNumber ?? 1) devuelve el valor de
pageNumber si tiene algún valor o devuelve 1 si pageNumber es NULL.

Agrega vínculos de paginación


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>
<tr>
<th>
<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-pageNumber="@(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-pageNumber="@(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 asistentes de etiquetas:

<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(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.

Crea una página About


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.
Cree el método About en el controlador Home.
Cree 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;
}

Agregue un método About en 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 .

Creación de la vista About


Agregue un archivo Views/Home/About.cshtml con 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.

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Agregado vínculos de ordenación de columnas
Agregado un cuadro de búsqueda
Agregado paginación al índice de Students
Agregado paginación al método Index
Agregado vínculos de paginación
Creado una página About
Pase al tutorial siguiente para obtener información sobre cómo controlar los cambios en el modelo de datos
mediante migraciones.
Siguiente: Control de los cambios en el modelo de datos
Tutorial: Uso de la característica de migraciones:
ASP.NET MVC con EF Core
10/05/2019 • 13 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre las migraciones
Cambiar la cadena de conexión
Crear una migración inicial
Examina los métodos Up y Down
Obtiene información sobre la instantánea del modelo de datos
Aplicar la migración

Requisitos previos
Ordenar, filtrar y paginar

Acerca de 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.
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.

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=tr
ue"
},
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 la
carpeta 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.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.2.0-rtm-35687 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.

Examina 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
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 Registro en
ASP.NET Core.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.2.0-rtm-35687 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (274ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (60ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [ContosoUniversity2] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [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[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190327172701_InitialCreate', N'2.2.0-rtm-35687');
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.
Comparar CLI y 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 se incluye en el metapaquete
Microsoft.AspNetCore.App, por lo que no es necesario agregar una referencia de paquete si la aplicación
tiene una para Microsoft.AspNetCore.App .
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).

Obtención del código


Descargue o vea la aplicación completa.

Paso siguiente
En este tutorial ha:
Obtenido información sobre las migraciones
Obtenido información sobre los paquetes de migración de NuGet
Cambiado la cadena de conexión
Creado una migración inicial
Examinado los métodos Up y Down
Obtenido información sobre la instantánea del modelo de datos
Aplicado la migración
Pase al tutorial siguiente para comenzar a examinar temas más avanzados sobre la expansión del modelo
de datos. Por el camino, podrá crear y aplicar migraciones adicionales.
Creación y aplicación de migraciones adicionales
Tutorial: Creación de un modelo de datos complejo:
ASP.NET MVC con EF Core
17/05/2019 • 53 minutes to read • Edit Online

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:

En este tutorial ha:


Personaliza el modelo de datos
Realiza cambios a la entidad Student
Crea la entidad Instructor
Crea la entidad OfficeAssignment
Modifica la entidad Course
Crea la entidad Department
Modifica la entidad Enrollment
Actualizar el contexto de base de datos
Inicializa la base de datos con datos de prueba
Agregar una migración
Cambiar la cadena de conexión
Actualizar la base de datos

Requisitos previos
Uso de migraciones de EF Core

Personaliza el modelo de datos


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)]
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) e intente escribir
cualquier nombre de más de 50 caracteres. La aplicación debería impedir que lo haga.
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)]
[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 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)]
[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.

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

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

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

Crea 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 departamento cuando se elimine el instructor, 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)

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

Acerca de una alternativa de la API fluida


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.

Inicializa 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"),
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[]


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


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

Actualizar la base de datos


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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado el modelo de datos
Realizado cambios a la entidad Student
Creado la entidad Instructor
Creado la entidad OfficeAssignment
Modificado la entidad Course
Creado la entidad Department
Modificado la entidad Enrollment
Actualizado el contexto de base de datos
Inicializado la base de datos con datos de prueba
Agregado una migración
Cambiado la cadena de conexión
Actualizado la base de datos
Pase al artículo siguiente para más información sobre cómo acceder a datos relacionados.
Siguiente: Acceso a datos relacionados
Tutorial: Lectura de datos relacionados: ASP.NET
MVC con EF Core
10/05/2019 • 26 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre cómo cargar datos relacionados
Crea una página de cursos
Crea una página de instructores
Obtiene información sobre la carga explícita

Requisitos previos
Creación de un modelo de datos complejo

Obtiene información sobre cómo cargar 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.

Crea una página de cursos


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.

Crea una página de instructores


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.

Acerca de la 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.

Obtención del código


Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Obtenido información sobre cómo cargar datos relacionados
Creado una página de cursos
Creado una página de instructores
Obtenido información sobre la carga explícita
Pase al tutorial siguiente para obtener información sobre cómo actualizar datos relacionados.
Actualización de datos relacionados
Tutorial: Actualización de datos relacionados:
ASP.NET MVC con EF Core
10/05/2019 • 31 minutes to read • Edit Online

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.
En este tutorial ha:
Personaliza las páginas de cursos
Agrega la página de edición de instructores
Agrega cursos a la página de edición
Actualiza la página Delete
Agrega la ubicación de la oficina y cursos a la página Create

Requisitos previos
Lectura de datos relacionados

Personaliza las páginas de cursos


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


.FirstOrDefaultAsync(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" al asistente de etiquetas <select> , y luego el asistente 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()
.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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()
.FirstOrDefaultAsync(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 un asistente
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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd class="col-sm-10">
@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.

Agrega la página de edición de instructores


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()
.FirstOrDefaultAsync(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)
.FirstOrDefaultAsync(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.
Agrega cursos a la página de edición
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()
.FirstOrDefaultAsync(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)
.FirstOrDefaultAsync(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.FirstOrDefault(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.FirstOrDefault(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.FirstOrDefault(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.FirstOrDefault(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, los saltos de línea podrían cambiarse de tal forma que el código se interrumpiese. Si el
código tiene un aspecto diferente después de pegarlo, 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. Este problema se ha corregido en Visual Studio 2019.
<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.

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

Agrega 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.
Obtención del código
Descargue o vea la aplicación completa.

Pasos siguientes
En este tutorial ha:
Personalizado las páginas de cursos
Agregado la página de edición de instructores
Agregado cursos a la página de edición
Actualizado la página Delete
Agregado la ubicación de la oficina y cursos a la página Create
Pase al tutorial siguiente para obtener información sobre cómo controlar conflictos de simultaneidad.
Control de conflictos de simultaneidad
Tutorial: Control de simultaneidad: ASP.NET MVC
con EF Core
20/06/2019 • 34 minutes to read • Edit Online

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.
En este tutorial ha:
Obtiene información sobre los conflictos de simultaneidad
Agrega una propiedad de seguimiento
Crea un controlador y vistas de Departments
Actualiza la vista de índice
Actualiza los métodos de edición
Actualiza la vista de edición
Prueba los conflictos de simultaneidad
Actualizar la página Delete
Actualizar las vistas Details y Create

Requisitos previos
Actualización de datos relacionados

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.

Agrega una propiedad de seguimiento


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

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

Actualiza la vista de índice


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.

Actualiza los métodos de edición


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

Actualiza la vista de edición


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

Prueba los conflictos de simultaneidad


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()
.FirstOrDefaultAsync(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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@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="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@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");}
}

Obtención del código


Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre cómo administrar la simultaneidad en EF Core, vea Concurrency conflicts
(Conflictos de simultaneidad).

Pasos siguientes
En este tutorial ha:
Obtenido información sobre los conflictos de simultaneidad
Agregado una propiedad de seguimiento
Creado un controlador y vistas de Departments
Actualizado la vista de índice
Actualizado los métodos de edición
Actualizado la vista de edición
Probado los conflictos de simultaneidad
Actualizado la página Delete
Actualizado las vistas Details y Create
Pase al tutorial siguiente para obtener información sobre cómo implementar la herencia de tabla por jerarquía
para las entidades Instructor y Student.
Siguiente: Implementación de la herencia de tabla por jerarquía
Tutorial: Implementación de la herencia: ASP.NET
MVC con EF Core
10/05/2019 • 14 minutes to read • Edit Online

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.
En este tutorial ha:
Asigna la herencia a la base de datos
Creación de la clase Person
Actualiza Instructor y Student
Agrega Person al modelo
Crea y actualiza migraciones
Prueba la implementación

Requisitos previos
Control de la simultaneidad

Asigna la herencia a la base 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;
}
}
}
}

Actualiza Instructor y Student


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


}
}

Agrega Person al modelo


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.

Crea y actualiza migraciones


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 la implementación
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.
Obtención del código
Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre la herencia en Entity Framework Core, consulte Herencia.

Pasos siguientes
En este tutorial ha:
Asignado la herencia a la base de datos
Creado la clase Person
Actualizado Instructor y Student
Agregado Person al modelo
Creado y actualizado migraciones
Probado la implementación
Pase al tutorial siguiente para obtener información sobre cómo controlar una serie de escenarios de Entity
Framework relativamente avanzados.
Siguiente: Temas avanzados
Tutorial: Información sobre escenarios avanzados:
ASP.NET MVC con EF Core
10/05/2019 • 25 minutes to read • Edit Online

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.
En este tutorial ha:
Realiza consultas SQL sin formato
Llama a una consulta para devolver entidades
Llama a una consulta para devolver otros tipos
Llamar a una consulta update
Examina consultas SQL
Crea una capa de abstracción
Obtiene información sobre la detección de cambios automática
Obtiene información sobre el código fuente y planes de desarrollo de Entity Framework Core
Obtiene información sobre cómo usar LINQ dinámico para simplificar el código

Requisitos previos
Implementación de la herencia

Realiza 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.
Llama a una consulta para devolver 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()
.FirstOrDefaultAsync();

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.

Llama a una consulta para devolver 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 Core en Instalado en el panel
izquierdo, haga clic en Vista de Razor 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.

Examina consultas SQL


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.

Crea una capa de abstracción


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 el código


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? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

if (searchString != null)
{
pageNumber = 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(),
pageNumber ?? 1, pageSize));
}
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. John Parente y Paul
Goldman trabajaron en la actualización del tutorial de ASP.NET Core 2.2.

Solucionar problemas de 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 SQL, error: 26: error al buscar el servidor o la instancia especificados)

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.

Obtención del código


Descargue o vea la aplicación completa.

Recursos adicionales
Para obtener más información sobre EF Core, consulte la documentación de Entity Framework Core. También
hay disponible un libro: Entity Framework Core en acción.
Para obtener información sobre cómo implementar una aplicación web, consulte Hospedaje e implementación
de ASP.NET Core.
Para obtener información sobre otros temas relacionados con ASP.NET Core MVC, como la autenticación y
autorización, vea Introducción a ASP.NET Core.

Pasos siguientes
En este tutorial ha:
Realizado consultas SQL sin formato
Llamado a una consulta para devolver entidades
Llamado a una consulta para devolver otros tipos
Llamado a una consulta update
Examinado consultas SQL
Creado una capa de abstracción
Obtenido información sobre la detección de cambios automática
Obtenido información sobre el código fuente y planes de desarrollo de Entity Framework Core
Obtenido información sobre cómo usar LINQ dinámico para simplificar el código
Con esto finaliza esta serie de tutoriales sobre cómo usar Entity Framework Core en una aplicación ASP.NET
Core MVC. En esta serie se ha trabajado con una base de datos nueva, pero también se pueden utilizar técnicas
de ingeniería inversa en un modelo de una base de datos existente.
Tutorial: EF Core con MVC, base de datos existente
Introducción a ASP.NET Core y Entity Framework 6
10/05/2019 • 8 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 de ASP.NET Core
En el cuadro de diálogo de selección de la plantilla de proyecto, seleccione la API y .NET Framework en la
lista desplegable.
Agregar > Nuevo proyecto > Escritorio 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)
Hospedaje e implementación de ASP.NET Core
27/05/2019 • 6 minutes to read • Edit Online

En general, para implementar una aplicación de ASP.NET Core en un entorno de hospedaje:


Implemente la aplicación publicada en una carpeta en el 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.
Para configurar un proxy inverso, configure un proxy inverso para reenviar solicitudes a la aplicación.

Publicar la aplicación en una carpeta


El comando 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
dotnet publish se lleva a cabo de forma automática antes de que los archivos se copien en el destino de
implementación.
Contenido de la carpeta
La carpeta publish contiene uno o varios archivos de ensamblado de aplicaciones, dependencias y,
opcionalmente, el entorno de ejecución. NET.
Se puede publicar una aplicación .NET Core como implementación independiente o implementación
dependiente del marco. Si la aplicación es independiente, los archivos de ensamblado que contienen el
entorno 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 obtener más información, vea Estructura de
directorios de ASP.NET Core.

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


Si la aplicación usa el servidor Kestrel, se 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.
Cualquiera de las configuraciones, —con o sin un servidor proxy inverso—, es una configuración de hospedaje
admitida. 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.

Uso de Visual Studio y MSBuild para automatizar implementaciones


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, consulte Perfiles de publicación
de Visual Studio para la implementación de aplicaciones ASP.NET Core y el libro Using MSBuild and Team
Foundation Build (Uso de 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. Para más información, consulte DevOps con ASP.NET
Core y Azure.

Publicar en Azure
Consulte Publicar una aplicación de ASP.NET Core en Azure con Visual Studio para obtener instrucciones
sobre cómo publicar una aplicación en Azure con Visual Studio. Crear una aplicación web de ASP.NET Core
en Azure proporciona un ejemplo adicional.

Publicar con MSDeploy en Windows


Consulte Perfiles de publicación de Visual Studio para la implementación de aplicaciones ASP.NET Core para
ver instrucciones sobre cómo publicar una aplicación con un perfil de publicación de Visual Studio, incluyendo
desde un símbolo del sistema de Windows mediante el comando dotnet msbuild.

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.

Realización de comprobaciones de estado


Use middleware de comprobación de estado para realizar comprobaciones de estado en una aplicación y sus
dependencias. Para obtener más información, vea Comprobaciones de estado en ASP.NET Core.

Recursos adicionales
Hospedar ASP.NET Core en contenedores de Docker
Solución de problemas de proyectos de ASP.NET Core
Implementar aplicaciones de ASP.NET Core en Azure
App Service
17/06/2019 • 19 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 App Service 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:
Creación de una aplicación web 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.
Creación de una aplicación ASP.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:
Publicar una aplicación de ASP.NET Core 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.
Implementación continua en Azure con Visual Studio y Git con ASP.NET Core
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
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
Plataforma
Los runtimes para aplicaciones de 64 bits (x64) y 32 bits (x86) están presentes en Azure App Service. El SDK de
.NET Core está disponible en App Service es para 32 bits, pero puede implementar aplicaciones de 64 bits con la
consola de Kudu o el comando MSDeploy con un perfil de publicador de Visual Studio o CLI.
En el caso de las aplicaciones con dependencias nativas, los runtimes de 32 bits (x86) están presentes Azure App
Service. El SDK de .NET Core disponible en App Service es de 32 bits.
Paquetes
Incluya los siguientes paquetes de NuGet para proporcionar 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.
Los paquetes anteriores no están disponibles en el metapaquete Microsoft.AspNetCore.App. Las aplicaciones
que tienen como destino .NET Framework o hacen referencia al metapaquete Microsoft.AspNetCore.App deben
hacer referencia de forma explícita a los paquetes individuales del archivo de proyecto de la aplicación.

Invalidación de la configuración de la aplicación mediante Azure Portal


La configuración de la aplicación en Azure Portal 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 configuración de aplicación se crea o modifica en Azure Portal y el botón Guardar está seleccionado,
se reinicia la aplicación de Azure. La variable de entorno está disponible para la aplicación después de que se
reinicie el servicio.
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.
Cuando una aplicación compila el host mediante WebHost.CreateDefaultBuilder, en las variables de entorno que
configuran el host se usa 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.

Escenarios de servidor proxy y equilibrador de carga


El middleware de integración con IIS, que configura el software intermedio de encabezados reenviados al
hospedar fuera de proceso, 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
Las aplicaciones ASP.NET Core implementadas de forma automática en App Service reciben una extensión de
App Service, Integración de registro de ASP.NET Core. La extensión habilita la integración de registro para las
aplicaciones ASP.NET Core en Azure App Service.
Las aplicaciones de ASP.NET Core implementadas automáticamente en App Service reciben una extensión de
App Service, Extensiones de registro de ASP.NET Core. La extensión habilita la integración de registro para
las aplicaciones ASP.NET Core en Azure App Service.
Para obtener información sobre supervisión, registro y solución de problemas, consulte los artículos siguientes:
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 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.
Controlar 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, vea Proveedores de almacenamiento de claves en ASP.NET Core.

Implementar una versión preliminar de ASP.NET Core en Azure App


Service
Siga uno de estos procedimientos:
Instalación de la extensión de sitio de versión preliminar.
Implementación de la aplicación independiente.
Uso de 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
aspnet/AspNetCore.
1. En Azure Portal, vaya a App Service.
2. Seleccione la aplicación web.
3. Escriba "ex" en el cuadro de búsqueda para filtrar por "Extensiones" o desplácese hacia abajo en la lista de
herramientas de administración.
4. Seleccione Extensiones.
5. Seleccione Agregar.
6. Seleccione la extensión Runtime de ASP.NET Core {X.Y } ({x64|x86}) en la lista, en la que {X.Y} es la
versión preliminar de ASP.NET Core y {x64|x86} especifica la plataforma.
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.
2. Seleccione Ir en 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} y la plataforma por {PLATFORM} en el comando:

Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.{PLATFORM}\

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 establece en la configuración de la aplicación
de Azure Portal 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) Runtime.
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\

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'))]"
]
}

Implementación de la aplicación independiente


Una implementación independiente (SCD ) que tiene como destino un entorno de ejecución en versión preliminar
transporta el entorno de ejecución en versión preliminar en la implementación.
Al implementar una aplicación independiente:
El sitio en Azure App Service no requiere la extensión del sitio en versión preliminar.
Se debe publicar la aplicación siguiendo un enfoque diferente que cuando se publica para una
implementación dependiente del marco (FDD ).
Publicación desde Visual Studio
1. Seleccione Compilar > Publicar {nombre de la aplicación} desde la barra de herramientas de Visual
Studio.
2. En el cuadro de diálogo Elegir un destino de publicación, confirme que está seleccionado App Service.
3. Seleccione Opciones avanzadas. Se abre el cuadro de diálogo Publicar.
4. En el cuadro de diálogo Publicar:
Confirme que está seleccionada la configuración de Versión.
Abra la lista desplegable Modo de implementación y seleccione Independiente.
Seleccione el entorno de ejecución de destino en la lista desplegable Tiempo de ejecución de
destino. De manera predeterminada, es win-x86 .
Si necesita quitar archivos adicionales tras la implementación, abra Opciones de publicación de
archivos y active la casilla de verificación para quitar archivos adicionales en el destino.
Seleccione Guardar.
5. Cree un sitio nuevo o actualice un sitio existente siguiendo las instrucciones restantes del asistente para
publicación.
Publicación mediante las herramientas de la interfaz de la línea de comandos (CLI)
1. En el archivo de proyecto, especifique uno o más identificadores de entorno de ejecución (RID ). Use
<RuntimeIdentifier> (en singular ) para un único RID o use <RuntimeIdentifiers> (en plural) para
proporcionar una lista de RID delimitada por puntos y coma. En el ejemplo siguiente, se especifica el RID
win-x86 :

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
</PropertyGroup>

2. Desde un shell de comandos, publique la aplicación en la configuración de la versión para el entorno de


ejecución del host con el comando dotnet publish. En el ejemplo siguiente, la aplicación está publicada
para el RID win-x86 . El RID proporcionado para la opción --runtime debe indicarse en la propiedad
<RuntimeIdentifier> (o <RuntimeIdentifiers> ) del archivo de proyecto.
dotnet publish --configuration Release --runtime win-x86

3. Mueva el contenido del directorio bin/Release/{TARGET FRAMEWORK }/{RUNTIME IDENTIFIER }/publish


al sitio de App Service.
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.

Configuración del protocolo (HTTPS)


Los enlaces de protocolo seguro permiten especificar un certificado para usarlo al responder a solicitudes a
través de HTTPS. Los enlaces requieren un certificado privado válido ( .pfx) que se haya emitido para el nombre
de host en cuestión. Para obtener más información, consulte Tutorial: Enlazar un certificado SSL personalizado
existente a Azure App Service.

Transformación de web.config
Si necesita transformar web.config al realizar la publicación (por ejemplo, establecer variables de entorno
basadas en la configuración, el perfil o el entorno), consulte Transformación de web.config.

Recursos adicionales
Información general de App Service
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
Módulos de IIS con ASP.NET Core
Windows Server: contenido de administradores de TI para versiones anteriores y actuales
Publicar una aplicación de ASP.NET Core en Azure
con Visual Studio
04/07/2019 • 7 minutes to read • Edit Online

Por Rick Anderson

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 de implementación de App Service, vea Solución de 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... .

Complete 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 editar el
párrafo para que incluya el mensaje "¡ Hola, ASP.NET Core!":

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

<p>Hello ASP.NET Core!</p>

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 con ASP.NET Core

Recursos adicionales
Para Visual Studio Code, vea Publicar los perfiles.
Azure App Service
Grupos de recursos de Azure
Azure SQL Database
Perfiles de publicación de Visual Studio para la implementación de aplicaciones ASP.NET Core
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
10/05/2019 • 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 Creación de la primera canalización con Azure Pipelines, donde se explica cómo configurar un flujo
de trabajo de entrega continua (CD ) para Azure App Service mediante Azure DevOps Services. Azure Pipelines,
un servicio de Azure DevOps 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
Creación de la primera canalización con Azure Pipelines
Proyecto Kudu
Perfiles de publicación de Visual Studio para la implementación de aplicaciones ASP.NET Core
Módulo ASP.NET Core
03/07/2019 • 42 minutes to read • Edit Online

Por Tom Dykstra, Rick Strahl, Chris Ross, Rick Anderson, Sourabh Shirhatti, Justin Kotalik y Luke Latham
El módulo ASP.NET Core es un módulo nativo de IIS que se conecta a la canalización de IIS para:
Hospedar una aplicación ASP.NET Core dentro del proceso de trabajo de IIS ( w3wp.exe ), lo que se denomina
modelo de hospedaje en proceso.
Reenviar solicitudes web a una aplicación ASP.NET Core de back-end que ejecuta el servidor de Kestrel, lo que
se denomina modelo de hospedaje fuera de proceso.
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
En el caso del hospedaje en proceso, el módulo usa el servidor HTTP de IIS ( IISHttpServer ), una implementación
de servidor en proceso de IIS.
Cuando se hospeda fuera de proceso, el módulo solo funciona con Kestrel. El módulo no es compatible con
HTTP.sys.

Modelos de hospedaje
Modelo de hospedaje en proceso
Para configurar una aplicación para el hospedaje en proceso, agregue la propiedad <AspNetCoreHostingModel> al
archivo de proyecto de la aplicación con un valor de InProcess (el hospedaje fuera de proceso se establece con
OutOfProcess ):

<PropertyGroup>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>

No se admite el modelo de hospedaje en proceso para aplicaciones de ASP.NET Core que tienen como destino
.NET Framework.
Si la propiedad <AspNetCoreHostingModel> no está presente en el archivo, el valor predeterminado es OutOfProcess .
Al hospedar en proceso, se aplican las siguientes características:
En lugar del servidor HTTP de IIS ( IISHttpServer ) se usa el servidor Kestrel. En el mismo proceso,
CreateDefaultBuilder llama a UseIIS para:
Registrar el IISHttpServer .
Configurar el puerto y la ruta de acceso base donde debe escuchar el servidor al ejecutarse detrás del
módulo de ASP.NET Core.
Configurar el host para capturar errores de inicio.
El atributo requestTimeout no se aplica al hospedaje en proceso.
No se admite el uso compartido de un grupo de aplicaciones entre aplicaciones. Se usa un grupo de
aplicaciones por aplicación.
Cuando se usa Web Deploy o se coloca manualmente un archivo app_offline.htm en la implementación,
puede que la aplicación no se apague inmediatamente si hay una conexión abierta. Por ejemplo, una
conexión WebSocket puede retrasar el apagado de la aplicación.
La arquitectura (valor de bits) de la aplicación y el runtime instalado (x64 o x86) deben coincidir con la
arquitectura del grupo de aplicaciones.
Si se configura el host de la aplicación manualmente con WebHostBuilder (no mediante
CreateDefaultBuilder) y la aplicación nunca se ejecuta directamente en el servidor de Kestrel
(autohospedada), llame a UseKestrel antes de llamar a UseIISIntegration . Si se invierte el orden, el host no
se inicia.
Se detectan las desconexiones del cliente. El token de cancelación HttpContext.RequestAborted se cancela
cuando el cliente se desconecta.
En ASP.NET Core 2.2.1 o versiones anteriores, GetCurrentDirectory devuelve el directorio de trabajo del
proceso iniciado por IIS en lugar del de la aplicación (por ejemplo, C:\Windows\System32\inetsrv para
w3wp.exe).
Para conocer el código de ejemplo que establece el directorio actual de la aplicación, consulte la información
sobre la clase CurrentDirectoryHelpers. Llame al método SetCurrentDirectory . Las llamadas subsiguientes a
GetCurrentDirectory proporcionan el directorio de la aplicación.
Cuando se hospeda en el proceso, no se llama a AuthenticateAsync de forma interna para inicializar un
usuario. Por tanto, se usa una implementación de IClaimsTransformation para transformar las notificaciones
después de que cada autenticación no se active de forma predeterminada. Al transformar notificaciones con
una implementación de IClaimsTransformation, llame a AddAuthentication para agregar servicios de
autenticación:

public void ConfigureServices(IServiceCollection services)


{
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
services.AddAuthentication(IISServerDefaults.AuthenticationScheme);
}

public void Configure(IApplicationBuilder app)


{
app.UseAuthentication();
}

Modelo de hospedaje fuera de proceso


Para configurar una aplicación para el hospedaje fuera de proceso, use uno de los métodos siguientes en el archivo
de proyecto:
No especifique la propiedad <AspNetCoreHostingModel> . Si la propiedad <AspNetCoreHostingModel> no está
presente en el archivo, el valor predeterminado es OutOfProcess .
Establezca el valor de la propiedad <AspNetCoreHostingModel> en OutOfProcess (el hospedaje en proceso se
establece con InProcess ):

<PropertyGroup>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>

En lugar del servidor Kestrel se usa el servidor HTTP de IIS ( IISHttpServer ).


Fuera del proceso, CreateDefaultBuilder llama a UseIISIntegration para:
Configurar el puerto y la ruta de acceso base donde debe escuchar el servidor al ejecutarse detrás del módulo
de ASP.NET Core.
Configurar el host para capturar errores de inicio.
Cambios del modelo de hospedaje
Si se modifica el valor hostingModel en el archivo web.config (se explica en la sección Configuración con
web.config), el módulo recicla el proceso de trabajo de IIS.
En IIS Express, el módulo no recicla el proceso de trabajo, sino que desencadena un cierre estable del proceso de
IIS Express actual. La siguiente solicitud a la aplicación genera un nuevo proceso de IIS Express.
Nombre del proceso
Process.GetCurrentProcess().ProcessName informa a w3wp / iisexpress (en proceso) o dotnet (fuera de proceso).
El módulo ASP.NET Core es un módulo nativo de IIS que se conecta a la canalización de IIS para reenviar
solicitudes web a aplicaciones ASP.NET Core de back-end.
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
El módulo funciona únicamente con Kestrel. El módulo no es compatible con HTTP.sys.
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 IIS, el módulo ASP.NET Core y una aplicación:

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 la 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. El middleware agregado por la integración de IIS actualiza el esquema, la dirección IP remota y
PathBase para responder del reenvío de la solicitud a Kestrel. 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.
Muchos de los módulos nativos, como la autenticación de Windows, permanecen activos. Para obtener más
información sobre los módulos de IIS activos con el módulo ASP.NET Core, vea Módulos de IIS con ASP.NET
Core.
El módulo ASP.NET Core también 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 sobre cómo instalar y usar el módulo ASP.NET Core, consulte Instalación del conjunto
de hospedaje de .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>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess" />
</system.webServer>
</location>
</configuration>

<?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>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\MyApp.exe"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess" />
</system.webServer>
</location>
</configuration>

La propiedad InheritInChildApplications está establecida en false para indicar que las aplicaciones que residen en
un subdirectorio de la aplicación no heredan la configuración especificada en el elemento <location>.

<?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.
Para obtener información sobre la configuración de aplicaciones secundarias de IIS, consulte Hospedaje de
ASP.NET Core en Windows con IIS.
Atributos del elemento aspNetCore
ATRIBUTO DESCRIPCIÓN DEFAULT

arguments Atributo de cadena opcional.


Argumentos para el archivo
ejecutable especificado en
processPath.

disableStartUpErrorPage Atributo Boolean opcional. 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.
ATRIBUTO DESCRIPCIÓN DEFAULT

forwardWindowsAuthToken Atributo Boolean opcional. 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.

hostingModel Atributo de cadena opcional. OutOfProcess

Especifica el modelo de hospedaje


como en proceso ( InProcess ) o
fuera de proceso ( OutOfProcess ).

processesPerApplication Atributo integer opcional. Valor predeterminado: 1


Mínimo: 1
Especifica el número de instancias Máximo: 100 †
del proceso especificado en el valor
processPath que pueden rotarse
por aplicación.
†En el hospedaje en proceso, el valor
está limitado a 1 .
No se recomienda establecer
processesPerApplication . Este
atributo se quitará en futuras
versiones.

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. Valor predeterminado: 10


Mínimo: 0
Especifica el número de veces que el Máximo: 100
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.
No admitido con el hospedaje en
proceso.
ATRIBUTO DESCRIPCIÓN DEFAULT

requestTimeout Atributo timespan opcional. Valor predeterminado: 00:02:00


Mínimo: 00:00:00
Especifica el tiempo que el módulo Máximo: 360:00:00
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.
No se aplica al hospedaje en
proceso. En el hospedaje en
proceso, el módulo espera a que la
aplicación procese la solicitud.
Los valores válidos para los
segmentos de minutos y segundos
de la cadena se encuentran en el
rango 0-59. El uso de 60 en el valor
de minutos o segundos da como
resultado el error 500: Error interno
del servidor.

shutdownTimeLimit Atributo integer opcional. Valor predeterminado: 10


Mínimo: 0
Tiempo en segundos que el módulo Máximo: 600
espera a que se cierre
correctamente el archivo ejecutable
cuando se detecta el archivo
app_offline.htm.

startupTimeLimit Atributo integer opcional. Valor predeterminado: 120


Mínimo: 0
Tiempo en segundos que espera el Máximo: 3600
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.
Un valor de 0 (cero) no se considera
un tiempo de expiración infinito.
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. Al crearse el
archivo de registro, el módulo crea
las carpetas que se proporcionan en
la ruta de acceso. 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.log
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 Atributo Boolean opcional. 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.
ATRIBUTO DESCRIPCIÓN DEFAULT

forwardWindowsAuthToken Atributo Boolean opcional. 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.

processesPerApplication Atributo integer opcional. Valor predeterminado: 1


Mínimo: 1
Especifica el número de instancias Máximo: 100
del proceso especificado en el valor
processPath que pueden rotarse
por aplicación.
No se recomienda establecer
processesPerApplication . Este
atributo se quitará en futuras
versiones.

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. Valor predeterminado: 10


Mínimo: 0
Especifica el número de veces que el Máximo: 100
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. Valor predeterminado: 00:02:00


Mínimo: 00:00:00
Especifica el tiempo que el módulo Máximo: 360:00:00
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.
ATRIBUTO DESCRIPCIÓN DEFAULT

shutdownTimeLimit Atributo integer opcional. Valor predeterminado: 10


Mínimo: 0
Tiempo en segundos que el módulo Máximo: 600
espera a que se cierre
correctamente el archivo ejecutable
cuando se detecta el archivo
app_offline.htm.

startupTimeLimit Atributo integer opcional. Valor predeterminado: 120


Mínimo: 0
Tiempo en segundos que espera el Máximo: 3600
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.
Un valor de 0 (cero) no se considera
un tiempo de expiración infinito.

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

WARNING
Las variables de entorno establecidas en esta sección entran en conflicto con las variables de entorno del sistema que tienen
el mismo nombre. Si se establece una variable de entorno en el archivo web.config y en el nivel del sistema en Windows, el
valor del archivo web.config se anexa al valor de la variable de entorno del sistema (por ejemplo,
ASPNETCORE_ENVIRONMENT: Development;Development ), que impide que se inicie la aplicación.

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"
hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="CONFIG_DIR" value="f:\application_config" />
</environmentVariables>
</aspNetCore>

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

NOTE
Una alternativa a establecer directamente el entorno en web.config consiste en incluir la propiedad <EnvironmentName> en el
perfil de publicación ( .pubxml) o el archivo de proyecto. Este método establece el entorno en web.config cuando se publica el
proyecto:

<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

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.
Al usar el modelo de hospedaje fuera de proceso, puede que la aplicación no se apague inmediatamente si hay una
conexión abierta. Por ejemplo, una conexión WebSocket puede retrasar el apagado de la aplicación.

Página de errores de inicio


Tanto el hospedaje en proceso como el hospedaje fuera de proceso generan páginas de error personalizado cuando
se produce un error al iniciar la aplicación.
Si el módulo ASP.NET Core no logra encontrar el controlador de solicitudes en proceso o fuera de proceso,
aparecerá la página de código de estado 500.0 - Error de carga de controlador en proceso/fuera de proceso.
Para el hospedaje en proceso, si el módulo ASP.NET Core no logra iniciar la aplicación, aparecerá la página de
código de estado 500.30 - Error de inicio.
En cuanto al hospedaje fuera de proceso, 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, aparecerá la 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 5xx de IIS predeterminada, use el atributo
disableStartUpErrorPage . Para obtener más información sobre cómo configurar los mensajes de error
personalizados, consulte Errores HTTP <httpErrors>.
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 obtener más información sobre cómo configurar los 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 . Al crearse el archivo de registro, el módulo crea las
carpetas de la ruta de acceso de stdoutLogFile . 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.
Si stdoutLogEnabled es falso, los errores que se produzcan al iniciar la aplicación se registrarán y se emitirán en el
registro de eventos hasta un máximo de 30 KB. Después del inicio, se descartarán los registros adicionales.
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"
hostingModel="InProcess">
</aspNetCore>

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
</aspNetCore>

Registros de diagnóstico mejorados


El módulo ASP.NET Core es configurable para proporcionar registros de diagnóstico mejorados. Agregue el
elemento <handlerSettings> al elemento <aspNetCore> de web.config. Al establecer debugLevel en TRACE se
expone una fidelidad mayor de información de diagnóstico:

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout"
hostingModel="InProcess">
<handlerSettings>
<handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
<handlerSetting name="debugLevel" value="FILE,TRACE" />
</handlerSettings>
</aspNetCore>

Al crearse el archivo de registro, el módulo crea las carpetas de la ruta de acceso (logs en el ejemplo anterior). 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).

El módulo no crea automáticamente las carpetas de la ruta de acceso proporcionada al valor <handlerSetting>
(logs en el ejemplo anterior), que deben existir previamente en la implementación. 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 valores de nivel de depuración ( debugLevel ) pueden incluir el nivel y la ubicación.
Niveles (en orden de menos a más detallado):
ERROR
WARNING
INFO
TRACE
Ubicaciones (se permiten varias ubicaciones):
CONSOLE
EVENTLOG
ARCHIVO
También se puede proporcionar la configuración de controlador a través de variables de entorno:
ASPNETCORE_MODULE_DEBUG_FILE – Ruta de acceso al archivo de registro de depuración. (El valor predeterminado
es aspnetcore-debug.log)
ASPNETCORE_MODULE_DEBUG – Valor de nivel de depuración.

WARNING
No deje habilitado el registro de depuración más tiempo del necesario en la implementación para solucionar un problema. El
tamaño del registro no es limitado. Dejar habilitado el registro de depuración puede agotar el espacio disponible en disco y
bloquear el servidor o el servicio de aplicación.

Consulte Configuración con web.config para ver un ejemplo del elemento aspNetCore en el archivo web.config.

Modificación del tamaño de la pila


Configure el tamaño de la pila administrada mediante el valor stackSize en bytes. El tamaño predeterminado es
1048576 bytes (1 MB ).

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout"
hostingModel="InProcess">
<handlerSettings>
<handlerSetting name="stackSize" value="2097152" />
</handlerSettings>
</aspNetCore>

La configuración de proxy usa el protocolo HTTP y un token de


emparejamiento
Solo se aplica al hospedaje fuera de proceso.
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 (
MS-ASPNETCORE-TOKEN ) 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 TrustedInstaller. Como 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 inicia un error de acceso denegado al intentar configurar los
valores del módulo en el archivo applicationHost.config del recurso compartido.
Cuando se usa una configuración compartida de IIS en el mismo equipo que la instalación de IIS, ejecute el
instalador del lote de hospedaje de ASP.NET Core con el parámetro OPT_NO_SHARED_CONFIG_CHECK establecido en 1 :

dotnet-hosting-{VERSION}.exe OPT_NO_SHARED_CONFIG_CHECK=1

Cuando la ruta de acceso a la configuración compartida no se encuentra en el mismo equipo que la instalación 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.
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
%ProgramFiles%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll
%ProgramFiles(x86)%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll
IIS Express (x86/amd64):
%ProgramFiles%\IIS Express\aspnetcore.dll
%ProgramFiles(x86)%\IIS Express\aspnetcore.dll
%ProgramFiles%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll
%ProgramFiles(x86)%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll
Schema
IIS
%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml
%windir%\System32\inetsrv\config\schema\aspnetcore_schema_v2.xml
IIS Express
%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml
%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema_v2.xml
Configuración
IIS
%windir%\System32\inetsrv\config\applicationHost.config
IIS Express
Visual Studio: {APPLICATION ROOT}\.vs\config\applicationHost.config
iisexpress.exe CLI: %USERPROFILE%\Documents\IISExpress\config\applicationhost.config
Los archivos se pueden encontrar mediante la búsqueda de aspnetcore en el archivo applicationHost.config.

Recursos adicionales
Hospedaje de ASP.NET Core en Windows con IIS
Repositorio GitHub del módulo ASP.NET Core (origen de referencia)
Módulos de IIS con ASP.NET Core
Solución de problemas de ASP.NET Core en Azure
App Service
02/07/2019 • 28 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. Para obtener consejos
adicionales de solución de problemas, consulte 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.
Temas adicionales de solución de problemas:
IIS también usa el módulo ASP.NET Core para hospedar aplicaciones. Para ver consejos de solución de
problemas pertenecientes específicamente a IIS, consulte Solución de problemas de ASP.NET Core en IIS.
En Controlar errores en ASP.NET Core se explica cómo controlar los errores de aplicaciones de ASP.NET
Core durante el desarrollo en un sistema local.
En Información sobre cómo depurar con Visual Studio se presentan las características del depurador de
Visual Studio.
En Depuración con Visual Studio Code se describe la compatibilidad de depuración integrada en Visual
Studio Code.

App startup errors


In Visual Studio, an ASP.NET Core project defaults to IIS Express hosting during debugging. A 502.5 -
Process Failure or a 500.30 - Start Failure that occurs when debugging locally can be troubleshooted using
the advice in this topic.
In Visual Studio, an ASP.NET Core project defaults to IIS Express hosting during debugging. A 502.5 Process
Failure that occurs when debugging locally can be troubleshooted using the advice in this topic.
500 Internal Server Error
The app starts, but an error prevents the server from fulfilling the request.
This error occurs within the app's code during startup or while creating a response. The response may
contain no content, or the response may appear as a 500 Internal Server Error in the browser. The Application
Event Log usually states that the app started normally. From the server's perspective, that's correct. The app
did start, but it can't generate a valid response. Run the app at a command prompt on the server or enable
the ASP.NET Core Module stdout log to troubleshoot the problem.
500.0 In-Process Handler Load Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module fails to find the .NET Core CLR and find the in-process request handler
(aspnetcorev2_inprocess.dll). Check that:
The app targets either the Microsoft.AspNetCore.Server.IIS NuGet package or the
Microsoft.AspNetCore.App metapackage.
The version of the ASP.NET Core shared framework that the app targets is installed on the target
machine.
500.0 Out-Of-Process Handler Load Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module fails to find the out-of-process hosting request handler. Make sure the
aspnetcorev2_outofprocess.dll is present in a subfolder next to aspnetcorev2.dll.
500.0 In-Process Handler Load Failure
The worker process fails. The app doesn't start.
An unknown error occurred loading ASP.NET Core Module components. Take one of the following actions:
Contact Microsoft Support (select Developer Tools then ASP.NET Core).
Ask a question on Stack Overflow.
File an issue on our GitHub repository.
500.30 In-Process Startup Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the .NET Core CLR in-process, but it fails to start. The cause of a
process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET
Core Module stdout log.
A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core
shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are
installed on the target machine.
500.31 ANCM Failed to Find Native Dependencies
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the .NET Core runtime in-process, but it fails to start. The most
common cause of this startup failure is when the Microsoft.NETCore.App or Microsoft.AspNetCore.App
runtime isn't installed. If the app is deployed to target ASP.NET Core 3.0 and that version doesn't exist on the
machine, this error occurs. An example error message follows:

The specified framework 'Microsoft.NETCore.App', version '3.0.0' was not found.


- The following frameworks were found:
2.2.1 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview5-27626-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27713-13 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27714-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27723-08 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]

The error message lists all the installed .NET Core versions and the version requested by the app. To fix this
error, either:
Install the appropriate version of .NET Core on the machine.
Change the app to target a version of .NET Core that's present on the machine.
Publish the app as a self-contained deployment.
When running in development (the ASPNETCORE_ENVIRONMENT environment variable is set to Development ), the
specific error is written to the HTTP response. The cause of a process startup failure is also found in the
Application Event Log.
500.32 ANCM Failed to Load dll
The worker process fails. The app doesn't start.
The most common cause for this error is that the app is published for an incompatible processor architecture.
If the worker process is running as a 32-bit app and the app was published to target 64-bit, this error occurs.
To fix this error, either:
Republish the app for the same processor architecture as the worker process.
Publish the app as a framework-dependent deployment.
500.33 ANCM Request Handler Load Failure
The worker process fails. The app doesn't start.
The app didn't reference the Microsoft.AspNetCore.App framework. Only apps targeting the
Microsoft.AspNetCore.App framework can be hosted by the ASP.NET Core Module.

To fix this error, confirm that the app is targeting the Microsoft.AspNetCore.App framework. Check the
.runtimeconfig.json to verify the framework targeted by the app.

500.34 ANCM Mixed Hosting Models Not Supported


The worker process can't run both an in-process app and an out-of-process app in the same process.
To fix this error, run apps in separate IIS application pools.
500.35 ANCM Multiple In-Process Applications in same Process
The worker process can't run both an in-process app and an out-of-process app in the same process.
To fix this error, run apps in separate IIS application pools.
500.36 ANCM Out-Of-Process Handler Load Failure
The out-of-process request handler, aspnetcorev2_outofprocess.dll, isn't next to the aspnetcorev2.dll file. This
indicates a corrupted installation of the ASP.NET Core Module.
To fix this error, repair the installation of the .NET Core Hosting Bundle (for IIS ) or Visual Studio (for IIS
Express).
500.37 ANCM Failed to Start Within Startup Time Limit
ANCM failed to start within the provied startup time limit. By default, the timeout is 120 seconds.
This error can occur when starting a large number of apps on the same machine. Check for CPU/Memory
usage spikes on the server during startup. You may need to stagger the startup process of multiple apps.
502.5 Process Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the worker process but it fails to start. The cause of a process
startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core
Module stdout log.
A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core
shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are
installed on the target machine. The shared framework is the set of assemblies (.dll files) that are installed on
the machine and referenced by a metapackage such as Microsoft.AspNetCore.App . The metapackage
reference can specify a minimum required version. For more information, see The shared framework.
The 502.5 Process Failure error page is returned when a hosting or app misconfiguration causes the worker
process to fail:
Failed to start application (ErrorCode '0x800700c1')

EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.

The app failed to start because the app's assembly (.dll) couldn't be loaded.
This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress
process.
Confirm that the app pool's 32-bit setting is correct:
1. Select the app pool in IIS Manager's Application Pools.
2. Select Advanced Settings under Edit Application Pool in the Actions panel.
3. Set Enable 32-Bit Applications:
If deploying a 32-bit (x86) app, set the value to True .
If deploying a 64-bit (x64) app, set the value to False .
Connection reset
If an error occurs after the headers are sent, it's too late for the server to send a 500 Internal Server Error
when an error occurs. This often happens when an error occurs during the serialization of complex objects for
a response. This type of error appears as a connection reset error on the client. Application logging can help
troubleshoot these types of errors.

Default startup limits


The ASP.NET Core Module is configured with a default startupTimeLimit of 120 seconds. When left at the
default value, an app may take up to two minutes to start before the module logs a process failure. For
information on configuring the module, see Attributes of the aspNetCore element.

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 solucionar problemas) de Azure Portal:
1. En Azure Portal, abra la aplicación en App Services.
2. Seleccione Diagnosticar y solucionar problemas.
3. Seleccione el título Herramientas de diagnóstico.
4. En Herramientas de soporte técnico, seleccione el botón Eventos de la aplicación.
5. Examine el error más reciente que hayan proporcionado las entradas IIS AspNetCoreModule o IIS
AspNetCoreModule V2 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. Abra 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. Abra 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.
Prueba de una aplicación de 32 bits (x86)
Ve r si ó n a c t u a l

1. cd d:\home\site\wwwroot
2. Ejecute la aplicación:
Si la aplicación es una implementación dependiente del marco:

dotnet .\{ASSEMBLY NAME}.dll

Si la aplicación es una implementación independiente:

{ASSEMBLY NAME}.exe

La salida de consola de la aplicación, que muestra los posibles errores, se canaliza a la consola de Kudu.
I m p l e m e n t a c i ó n d e p e n d i e n t e d e m a r c o d e t r a b a j o e n e j e c u c i ó n e n u n a v e r si ó n p r e l i m i n a r

Requiere la instalación de la extensión de sitio ASP.NET Core {VERSION } (x86 ) Runtime.


1. cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x32 ( {X.Y} es la versión del runtime)
2. Ejecute la aplicación: dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll

La salida de consola de la aplicación, que muestra los posibles errores, se canaliza a la consola de Kudu.
Prueba de una aplicación de 64 bits (x64)
Ve r si ó n a c t u a l

Si la aplicación es una implementación dependiente del marco de 64 bits (x64):


1. cd D:\Program Files\dotnet
2. Ejecute la aplicación: dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll
Si la aplicación es una implementación independiente:
1. cd D:\home\site\wwwroot
2. Ejecute la aplicación: {ASSEMBLY NAME}.exe

La salida de consola de la aplicación, que muestra los posibles errores, se canaliza a la consola de Kudu.
I m p l e m e n t a c i ó n d e p e n d i e n t e d e m a r c o d e t r a b a j o e n e j e c u c i ó n e n u n a v e r si ó n p r e l i m i n a r

Requiere la instalación de la extensión de sitio ASP.NET Core {VERSION } (x64 ) Runtime.


1. cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64 ( {X.Y} es la versión del runtime)
2. Ejecute la aplicación: dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll

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.
Deshabilite el registro de stdout una vez haya solucionado los problemas:
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.

Registro de depuración del módulo de ASP.NET Core


El registro de depuración del módulo de ASP.NET Core ofrece un registro adicional, más profundo, del
módulo ASP.NET Core. Para habilitar y ver los registros de stdout:
1. Para habilitar el registro de diagnóstico mejorado, realice cualquiera de las acciones siguientes:
Siga las instrucciones de Registros de diagnóstico mejorados para configurar la aplicación para un
registro de diagnóstico mejorado. Vuelva a implementar la aplicación.
Agregue la <handlerSettings> que se muestra en Registros de diagnóstico mejorados al archivo
web.config de la aplicación activa mediante la consola de Kudu:
a. Abra 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.
b. Mediante la barra de navegación de la parte superior de la página, abra la consola de
depuración y seleccione CMD.
c. Abra las carpetas para la ruta de acceso site > wwwroot. Seleccione el icono de lápiz para
editar el archivo web.config. Agregue la sección <handlerSettings> como se muestra en
Registros de diagnóstico mejorados. Seleccione el botón Guardar.
2. Abra 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.
3. Mediante la barra de navegación de la parte superior de la página, abra la consola de depuración y
seleccione CMD.
4. Abra las carpetas para la ruta de acceso site > wwwroot. Si no ha proporcionado una ruta de acceso para
el archivo aspnetcore debug.log, el archivo aparece en la lista. Si ha proporcionado una ruta de acceso,
vaya a la ubicación del archivo de registro.
5. Abra el archivo de registro con el botón de lápiz situado junto al nombre del archivo.
Deshabilite el registro de depuración una vez haya solucionado los problemas:
1. Para deshabilitar el registro de depuración mejorado, realice cualquiera de las acciones siguientes:
Quite localmente la <handlerSettings> del archivo web.config y vuelva a implementar la aplicación.
Use la consola de Kudu para editar el archivo web.config y quite la sección <handlerSettings> .
Guarde el archivo.

WARNING
Si no deshabilita el registro de depuración, es posible que se produzcan errores en la aplicación o el servidor. No hay
ningún límite para el tamaño del archivo de registro. Use únicamente el registro de depuración 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


Vea Referencia de errores comunes de Azure App Service e IIS con 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 responde con lentitud o se bloquea en una solicitud, consulte los artículos siguientes:
Solucionar los problemas de rendimiento reducido de aplicaciones web en Azure App Service
Use Crash Diagnoser Site Extension to Capture Dump for Intermittent Exception issues or performance
issues on Azure Web App (Uso de la extensión de sitio de diagnóstico de bloqueo para capturar el volcado
de memoria para problemas de excepciones intermitentes o problemas de rendimiento en Azure Web
Apps)

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.
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
Controlar 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)
Referencia de errores comunes de Azure App Service
e IIS con ASP.NET Core
22/05/2019 • 33 minutes to read • Edit Online

Por Luke Latham


En este tema, se ofrece información sobre cómo solucionar errores comunes al hospedar aplicaciones ASP.NET
Core en Azure App Service e IIS.
Recopile la siguiente información:
Comportamiento del explorador (código de estado y mensaje de error)
Entradas de registro de eventos de la aplicación
Azure App Service : vea Solución de problemas de ASP.NET Core en Azure App Service.
IIS
1. Seleccione Inicio en el menú Windows, escriba Visor de eventos y presione Entrar.
2. Una vez abierto el Visor de eventos, amplíe Registros de Windows > Aplicación en la barra
lateral.
Entradas de registro de stdout y depuración de módulo ASP.NET Core
Azure App Service : vea Solución de problemas de ASP.NET Core en Azure App Service.
IIS: siga las instrucciones de las secciones Creación y redireccionamiento de registros y Registros de
diagnóstico mejorados del tema Módulo ASP.NET Core.
Compare la información sobre errores con los siguientes errores comunes. Si se encuentra una coincidencia, siga
los consejos de solución de problemas.
La lista de errores en este tema no es exhaustiva. Si se produce algún error que no aparezca aquí, abra un problema
nuevo mediante el botón Comentarios sobre el contenido situado en la parte inferior de este tema con
instrucciones detalladas sobre cómo reproducir el error.

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: No se ha podido ejecutar el
paquete EXE
†El registro se encuentra en
C:\Users{USER }\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 .NET Core, 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 el servidor 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 o en Aplicaciones y características. Si se requiere un tiempo de ejecución específico, descargue
el tiempo de ejecución de Archivos de descarga de .NET e instálelo en el sistema. 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: el archivo DLL del módulo C:\WINDOWS\system32\inetsrv\aspnetcore.dll no se ha
podido cargar. 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. Este problema se produce si ha instalado el módulo ASP.NET Core antes de
una actualización del sistema operativo y, a continuación, ejecuta cualquier grupo de aplicaciones en modo de 32
bits después de una actualización del sistema operativo. 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.

Falta la extensión de sitio, se han instalado extensiones de sitio de 32


bits (x86) y 64 bits (x64) o se ha definido un valor de bits de proceso
incorrecto
Se aplica a las aplicaciones hospedadas por Azure App Services.
Explorador: Error HTTP 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud en
proceso ni se han encontrado dependencias nativas. No se ha podido encontrar el controlador de la solicitud
en proceso. Resultado obtenido al invocar a hostfxr: No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-preview -
*" no se ha podido encontrar. No se pudo iniciar la aplicación "/LM/W3SVC/1416782824/ROOT", código de
error "0x8000ffff".
Registro de stdout del módulo de ASP.NET Core: No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-preview -
*" no se ha podido encontrar.
Registro de depuración del módulo de ASP.NET Core: No se ha podido invocar a hostfxr para encontrar el
controlador de la solicitud en proceso ni se han encontrado dependencias nativas. Lo más probable es que esto
signifique que la aplicación no está configurada correctamente. Compruebe las versiones de
Microsoft.NetCore.App y de Microsoft.AspNetCore.App que la aplicación tiene como destino y que están
instaladas en el equipo. Se ha devuelto un valor HRESULT con errores: 0x8000ffff. No se ha podido encontrar el
controlador de la solicitud en proceso. No se ha podido encontrar ninguna versión de Framework compatible. El
marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-preview -*" no se ha podido encontrar.
Solución del problema:
Si ejecuta la aplicación en un entorno de ejecución en versión preliminar, instale la extensión de sitio de 32
bits (x86) o de 64 bits (x64) que coincida con el valor de bits de la aplicación y la versión del entorno de
ejecución de la aplicación. No instale al mismo tiempo ambas extensiones o varias versiones del
entorno de ejecución de la extensión.
ASP.NET Core {RUNTIME VERSION } (x86) Runtime
ASP.NET Core {RUNTIME VERSION } (x64) Runtime
Reinicie la aplicación. Espere unos segundos a que finalice la operación.
Si ejecuta la aplicación en un entorno de ejecución en versión preliminar y están instaladas las extensiones
de sitio de 32 bits (x86) y 64 bits (x64), desinstale la extensión de sitio que no coincida con el valor de bits de
la aplicación. A continuación, reinicie la aplicación. Espere unos segundos a que finalice la operación.
Si va a ejecutar la aplicación en un entorno de ejecución en versión preliminar y el valor de bits de la
extensión de sitio coincide con el de la aplicación, confirme que la versión del entorno de ejecución de la
extensión de sitio coincide con la versión del entorno de ejecución de la aplicación.
Confirme que el valor de Plataforma de la aplicación en Configuración de la aplicación coincide con el
valor de bits de la aplicación.
Para obtener más información, vea Implementar aplicaciones de ASP.NET Core en Azure App Service.

Se ha implementado una aplicación x86, pero el grupo de aplicaciones


no está habilitado para aplicaciones de 32 bits
Explorador: Error HTTP 500.30: error de inicio en el proceso ANCM
Registro de aplicación: En la aplicación '/LM/W3SVC/5/ROOT' con la raíz física '{PATH}' se ha producido
una excepción administrada inesperada, código de excepción = '0xe0434352'. Compruebe los registros
stderr para obtener más información. La aplicación '/LM/W3SVC/5/ROOT' con la raíz física '{PATH}' no ha
podido cargar el CLR ni la aplicación administrada. El subproceso de trabajo CLR se ha cerrado
prematuramente
Registro de stdout del módulo ASP.NET Core: El archivo de registro se ha creado, pero está vacío.
Registro de depuración del módulo de ASP.NET Core: Se ha devuelto un valor HRESULT con errores:
0x8007023e
El SDK intercepta este escenario al publicar una aplicación independiente. El SDK genera un error si el RID no
coincide con el destino de plataforma (por ejemplo, RID de win10-x64 con <PlatformTarget>x86</PlatformTarget>
en el archivo del proyecto).
Solución del problema:
En el caso de una implementación dependiente del marco x86 ( <PlatformTarget>x86</PlatformTarget> ), habilite el
grupo de aplicaciones de IIS para aplicaciones de 32 bits. En el Administrador de IIS, abra la Configuración
avanzada del grupo de aplicaciones y establezca Habilitar aplicaciones de 32 bits en True.

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 ha podido iniciar el proceso con la línea de comandos '"C:{{PATH}{ASSEMBLY }.{exe|dll}"', código
de error = '0x80004005 : ff'.
Registro de stdout del módulo ASP.NET Core: Excepción no controlada:
System.BadImageFormatException: No se ha podido cargar el archivo ni el 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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 --O -- No se podido establecer la conexión
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
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: Para usar el módulo ASP.NET Core, se deben instalar las características de IIS
7.0 CoreWebEngine y W3SVC.
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: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
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 de ASP.NET Core no instalado o permisos


incorrectos
Explorador: 500.19 Error interno del servidor: no se puede acceder a la página solicitada porque los datos
de configuración relacionados de la página no son válidos. --O -- No se puede mostrar esta página
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
Confirme que está habilitado el rol adecuado. Vea Configuración de IIS.
Abra Programas y características o Aplicaciones y características y confirme que Hospedaje de
Windows Server está instalado. Si Hospedaje de Windows Server no está presente en la lista de
programas instalados, descargue e instale el conjunto de hospedaje de .NET Core.
Instalador del conjunto de hospedaje de .NET Core actual (descarga directa)
Para obtener más información, consulte Instalar el conjunto 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.
Si ha desinstalado el paquete de hospedaje de ASP.NET Core y ha instalado una versión anterior de dicho
paquete, el archivo applicationHost.config no incluirá ninguna sección para el módulo ASP.NET Core. Abra
applicationHost.config en %windir%/System32/inetsrv/config y busque el grupo de secciones
<configuration><configSections><sectionGroup name="system.webServer"> . Si falta la sección del módulo
ASP.NET Core en el grupo de secciones, añada el elemento de sección:

<section name="aspNetCore" overrideModeDefault="Allow" />

Si quiere, también puede instalar la versión más reciente del paquete de hospedaje de ASP.NET Core. La
versión más reciente es compatible con las versiones anteriores de las aplicaciones ASP.NET Core
admitidas.

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 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: La aplicación 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY }' con la raíz física
'C:{{PATH}'' no ha podido iniciar el proceso con la línea de comandos '"{...}" ', código de error = '0 x
80070002 : 0. La aplicación '{PATH}' no se ha podido iniciar. No se ha encontrado el archivo ejecutable en
'{PATH}'. No se ha podido iniciar la aplicación '/LM/W3SVC/2/ROOT', código de error '0x8007023e'.
Registro de stdout del módulo ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: Registro de eventos: "La aplicación '{PATH}'" no
se ha podido iniciar. No se ha encontrado el archivo ejecutable en '{PATH}'. Se ha devuelto un valor
HRESULT con errores: 0x8007023e
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 ha podido iniciar el proceso con la línea de comandos '"{...}" ', código de error = '0 x
80070002 : 0.
Registro de stdout del módulo ASP.NET Core: El archivo de registro se ha creado, pero está 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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, es posible que la identidad del usuario del grupo de aplicaciones no pueda acceder a
dotnet.exe. Confirme que la identidad del usuario del grupo de aplicaciones tiene acceso al directorio
C:\Archivos de programa\dotnet. Confirme que no haya ninguna regla de denegación configurada para la
identidad del usuario del grupo de aplicaciones 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.
Instalador del conjunto de hospedaje de .NET Core actual (descarga directa)
Para obtener más información, consulte Instalar el conjunto de hospedaje de .NET Core.
Si se requiere un tiempo de ejecución específico, descargue el tiempo de ejecución de Archivos de descarga
de .NET e instálelo en el sistema. 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 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud en
proceso ni se han encontrado dependencias nativas. Lo más probable es que esto signifique que la
aplicación no está configurada correctamente. Compruebe las versiones de Microsoft.NetCore.App y de
Microsoft.AspNetCore.App que la aplicación tiene como destino y que están instaladas en el equipo. No se
ha podido encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a hostfxr:
¿pretendía ejecutar comandos SDK de dotnet? Instale el SDK de dotnet de: https://go.microsoft.com/fwlink/?
LinkID=798306&clcid=0x409 No se ha podido iniciar la aplicación '/LM/W3SVC/3/ROOT', código de error
'0x8000ffff'.
Registro de stdout del módulo ASP.NET Core: ¿pretendía ejecutar comandos SDK de dotnet? Instale el
SDK de dotnet de: https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
Registro de depuración del módulo de ASP.NET Core: No se ha podido invocar a hostfxr para encontrar
el controlador de la solicitud en proceso ni se han encontrado dependencias nativas. Lo más probable es que
esto signifique que la aplicación no está configurada correctamente. Compruebe las versiones de
Microsoft.NetCore.App y de Microsoft.AspNetCore.App que la aplicación tiene como destino y que están
instaladas en el equipo. Se ha devuelto un valor HRESULT con errores: 0x8000ffff No se ha podido
encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a hostfxr: ¿pretendía
ejecutar comandos SDK de dotnet? Instale el SDK de dotnet de: https://go.microsoft.com/fwlink/?
LinkID=798306&clcid=0x409 Se ha devuelto un valor HRESULT con errores: 0x8000ffff
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 ha podido iniciar el proceso con la línea de comandos '"dotnet" .{ASSEMBLY }.dll',
código de error = '0x80004005 : 80008081.
Registro de stdout del módulo de 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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 es una lista de argumentos de la aplicación (
arguments="{ARGUMENT_1}, {ARGUMENT_2}, ... {ARGUMENT_X}" ) para una implementación independiente ( SCD ).

Falta el marco compartido de .NET Core


Explorador: Error HTTP 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud en
proceso ni se han encontrado dependencias nativas. Lo más probable es que esto signifique que la
aplicación no está configurada correctamente. Compruebe las versiones de Microsoft.NetCore.App y de
Microsoft.AspNetCore.App que la aplicación tiene como destino y que están instaladas en el equipo. No se
ha podido encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a hostfxr: No se
ha podido encontrar ninguna versión de Framework compatible. El marco especificado
'Microsoft.AspNetCore.App', versión '{VERSION }', no se ha podido encontrar.
No se ha podido iniciar la aplicación '/LM/W3SVC/5/ROOT', código de error '0x8000ffff'.
Registro de stdout del módulo de ASP.NET Core: No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado 'Microsoft.AspNetCore.App', versión '{VERSION }', no se ha
podido encontrar.
Registro de depuración del módulo de ASP.NET Core: Se ha devuelto un valor HRESULT con errores:
0x8000ffff
Solución del problema:
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.

Grupo de aplicaciones detenido


Explorador: 503 Servicio no disponible
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
Confirme que el grupo de aplicaciones no está en estado Detenido.

La aplicación secundaria incluye una sección de <controladores>


Explorador: error HTTP 500.19: error interno del servidor
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: El archivo de registro de la aplicación raíz se ha creado
y muestra un funcionamiento normal. No se ha creado el archivo de registro de la aplicación secundaria.
Registro de depuración del módulo de ASP.NET Core: El archivo de registro de la aplicación raíz se ha
creado y muestra un funcionamiento normal. No se ha creado el archivo de registro de la aplicación secundaria.
Solución del problema:
Confirme que el archivo web.config de la aplicación secundaria no incluye una sección <handlers> o que la
aplicación secundaria no hereda los controladores de la aplicación primaria.
La sección <system.webServer>de la aplicación primaria de web.config está colocada dentro de un elemento
<location> . La propiedad InheritInChildApplications está establecida en false para indicar que las aplicaciones
que residen en un subdirectorio de la aplicación primaria no heredan la configuración especificada en el elemento
<location>. Para obtener más información, vea Módulo ASP.NET Core.
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: No se ha podido iniciar la redirección de stdout en C:\Archivos de
programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Mensaje de excepción: HRESULT 0 x 80070005
se ha devuelto en {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. No se ha podido
detener la redirección de stdout en C:\Archivos de programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll.
Mensaje de excepción: HRESULT 0 x 80070002 se ha devuelto en {PATH}. No se ha podido iniciar la
redirección de stdout en {PATH}\aspnetcorev2_inprocess.dll.
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha podido iniciar la redirección de stdout
en C:\Archivos de programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Mensaje de excepción:
HRESULT 0 x 80070005 se ha devuelto en
{PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. No se ha podido detener la redirección
de stdout en C:\Archivos de programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Mensaje de
excepción: HRESULT 0 x 80070002 se ha devuelto en {PATH}. No se ha podido iniciar la redirección de
stdout en {PATH}\aspnetcorev2_inprocess.dll.
Registro de aplicación: Advertencia: No se ha podido crear el archivo de registro de stdout \?
{PATH}\path_doesnt_exist\stdout_{PROCESS ID }{TIMESTAMP }.log, código de error = -2147024893.
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
La ruta de acceso stdoutLogFile especificada en el elemento <aspNetCore> de web.config no existe. Para
obtener más información, consulte ASP.NET Core Module: Log creation and redirection (Creación y
redireccionamiento de registros: módulo de ASP.NET Core).
El usuario del grupo de aplicaciones no tiene acceso de escritura a la ruta de acceso del registro de stdout.

Problema general de configuración de aplicación


Explorador: error HTTP 500.0: error de carga del controlador en proceso ANCM --O -- Error HTTP 500.30:
error de inicio en el proceso ANCM
Registro de aplicación: Variable
Registro de stdout del módulo de ASP.NET Core: El archivo de registro se ha creado, pero está vacío o
se ha creado con entradas normales, hasta el punto en que se producen errores en la aplicación.
Registro de depuración del módulo de ASP.NET Core: Variable
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}'' ha creado 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 "{PORT}", código de error =
'{ERROR CODE }'
Registro de stdout del módulo de ASP.NET Core: El archivo de registro se ha creado, pero está vacío.
Solución del problema:
El proceso no se ha iniciado, probablemente debido a un problema de configuración o programación de la
aplicación.
Para obtener más información, vea los temas siguientes:
Solución de problemas de ASP.NET Core en IIS
Solución de problemas de ASP.NET Core en Azure App Service
Solución de problemas de proyectos de ASP.NET Core
DevOps con ASP.NET Core y Azure
10/05/2019 • 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
10/05/2019 • 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
10/05/2019 • 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: En 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
10/05/2019 • 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 centro 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 centro 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: Implementación de Azure App Service a 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
Creación de la primera canalización con Azure Pipelines
Proyecto de compilación y .NET Core
Implementar una aplicación web con canalizaciones de Azure
Supervisar y depurar
10/05/2019 • 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 de datos que entran en la aplicación web.
Datos de salida: Salida de datos de la aplicación web a los clientes.
Las solicitudes: Número de solicitudes HTTP.
Tiempo promedio de respuesta: Tiempo medio de 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: Actualmente, las alertas en métricas de la aplicación web solo está disponible en el servicio de alertas
(clásico ).
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
10/05/2019 • 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.
Hospedaje de ASP.NET Core en Windows con IIS
02/07/2019 • 65 minutes to read • Edit Online

Por Luke Latham


Instalación del conjunto de hospedaje de .NET Core

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.
Para obtener información sobre el hospedaje en Azure, vea Implementar aplicaciones de ASP.NET Core en
Azure App Service.

Plataformas compatibles
Se admiten las aplicaciones publicadas para implementaciones de 32 bits (x86) y 64 bits (x64). Implemente una
aplicación de 32 bits con un SDK de .NET Core de 32 bits (x86) a menos que la aplicación:
Requiera el espacio de direcciones de memoria virtual más grande disponible para una aplicación de 64
bits.
Requiera el tamaño de la pila IIS más grande.
Tenga dependencias nativas de 64 bits.
Use un SDK de .NET Core de 64 bits (x64) para publicar una aplicación de 64 bits. Debe haber un tiempo de
ejecución de 64 bits en el sistema host.

Modelos de hospedaje
Modelo de hospedaje en proceso
Con el hospedaje en proceso, una aplicación ASP.NET Core se ejecuta en el mismo proceso que su proceso de
trabajo de IIS. El hospedaje en proceso proporciona un rendimiento mejorado con respecto al hospedaje fuera
de proceso porque las solicitudes no se realizan mediante proxy en el adaptador de bucle invertido, una
interfaz de red que devuelve el tráfico saliente a la misma máquina. IIS controla la administración de procesos
con el Servicio de activación de procesos de Windows (WAS ).
El módulo ASP.NET Core:
Realiza la inicialización de aplicaciones.
Carga CoreCLR.
Llama a Program.Main .
Controla la duración de la solicitud nativa de IIS.
No se admite el modelo de hospedaje en proceso para aplicaciones de ASP.NET Core que tienen como destino
.NET Framework.
En el siguiente diagrama se muestra la relación entre IIS, el módulo ASP.NET Core y una aplicación
hospedada en proceso:

Una solicitud llega de Internet al controlador HTTP.sys en modo kernel. El controlador enruta la solicitud
nativa a IIS en el puerto configurado del sitio web, que suele ser el puerto 80 (HTTP ) o 443 (HTTPS ). El
módulo recibe la solicitud nativa y la pasa a IIS HTTP Server ( IISHttpServer ). El servidor HTTP de IIS es una
implementación de servidor en proceso para IIS que convierte una solicitud nativa en administrada.
Una vez que IIS HTTP Server procesa la solicitud, 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 pasa a IIS a través del servidor HTTP de IIS. IIS envía la
respuesta al cliente que inició la solicitud.
El hospedaje en proceso es opcional para las aplicaciones existentes, pero, para las plantillas dotnet new, este
modelo es el predeterminado para todos los escenarios de IIS e IIS Express.
CreateDefaultBuilder agrega una instancia IServer mediante una llamada al método UseIIS para iniciar
CoreCLR y hospedar la aplicación dentro del proceso de trabajo de IIS (w3wp.exe o iisexpress.exe). Las pruebas
de rendimiento indican que el hospedaje de una aplicación .NET Core en proceso proporciona un rendimiento
de solicitud considerablemente mayor en comparación con el hospedaje de solicitudes de aplicaciones fuera de
proceso y de proxy para el servidor Kestrel.

NOTE
Las aplicaciones publicadas como un único archivo ejecutable no se pueden cargar por el modelo de hospedaje en
proceso.

Modelo de hospedaje fuera de proceso


Dado que las aplicaciones ASP.NET Core se ejecutan en un proceso independiente del proceso de trabajo de
IIS, el módulo 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 apaga o se bloquea. Este
comportamiento es básicamente el mismo que el de las aplicaciones que se ejecutan en proceso 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 IIS, el módulo ASP.NET Core y una aplicación
hospedada fuera de proceso:

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 la extensión UseIISIntegration
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 la 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. El middleware agregado por la integración de IIS actualiza el esquema, la dirección IP remota y
PathBase para responder del reenvío de la solicitud a Kestrel. 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.
ASP.NET Core se distribuye con el servidor Kestrel, un servidor HTTP multiplataforma predeterminado.
Al usar IIS o IIS Express, la aplicación se ejecuta en un proceso distinto al del proceso de trabajo de IIS (fuera
de proceso) con el servidor Kestrel.
Dado que las aplicaciones ASP.NET Core se ejecutan en un proceso independiente del proceso de trabajo de
IIS, el módulo 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 apaga o se bloquea. Este
comportamiento es básicamente el mismo que el de las aplicaciones que se ejecutan en proceso 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 IIS, el módulo ASP.NET Core y una aplicación
hospedada fuera de proceso:

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 la 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. El middleware agregado por la integración de IIS actualiza el esquema, la dirección IP remota y
PathBase para responder del reenvío de la solicitud a Kestrel. 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.
CreateDefaultBuilder configura el servidor Kestrel como servidor web y habilita IIS Integration mediante la
configuración de la ruta de acceso y el puerto base para el módulo ASP.NET Core.
El módulo ASP.NET Core genera un puerto dinámico que se asigna al proceso back-end.
CreateDefaultBuilder llama al método UseIISIntegration. UseIISIntegration configura Kestrel para escuchar
en el puerto dinámico en la dirección IP de localhost ( 127.0.0.1 ). Si el puerto dinámico es 1234, Kestrel
escucha en 127.0.0.1:1234 . Esta configuración reemplaza a otras configuraciones de dirección URL
proporcionadas por:
UseUrls
API de escucha de Kestrel
Configuración (u opción --urls de la línea de comandos)
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 los modelos de hospedaje dentro y fuera de proceso, vea el módulo
ASP.NET Core y la Referencia de configuración del módulo ASP.NET Core.
Para obtener instrucciones sobre la configuración del módulo ASP.NET Core, vea Módulo ASP.NET Core.
Para obtener más información sobre el hospedaje, consulte Hospedaje en ASP.NET Core.

Configuración de aplicación
Habilitación de los componentes de integración con IIS
Un archivo Program.cs típico llama a CreateDefaultBuilder para iniciar la configuración de un host que permite
la integración con IIS:

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


WebHost.CreateDefaultBuilder(args)
...

Opciones de IIS
Modelo de hospedaje en proceso
Para configurar las opciones del servidor de IIS, incluya una configuración de servicio para IISServerOptions
en ConfigureServices. Con el ejemplo siguiente se deshabilita AutomaticAuthentication:

services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = false;
});

OPCIÓN DEFAULT PARÁMETRO

AutomaticAuthentication true Si es true, el servidor IIS establece el


HttpContext.User autenticado
mediante la autenticación de
Windows. Si es false , el servidor
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 obtener más información, vea
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.

AllowSynchronousIO false Si se permiten la E/S sincrónica para


HttpContext.Request y
HttpContext.Response .
OPCIÓN DEFAULT PARÁMETRO

MaxRequestBodySize 30000000 Obtiene o establece el tamaño


máximo del cuerpo de solicitud para
HttpRequest . Tenga en cuenta que el
propio IIS tiene el límite de
maxAllowedContentLength , que se
procesará antes que el valor de
MaxRequestBodySize establecido en
IISServerOptions . El cambio de
MaxRequestBodySize no afectará a
maxAllowedContentLength . Para
aumentar maxAllowedContentLength
, agregue una entrada al archivo
web.config para establecer
maxAllowedContentLength en un
valor superior. Para más información,
consulte Configuración.

Modelo de hospedaje fuera de proceso

OPCIÓN DEFAULT PARÁMETRO

AutomaticAuthentication true Si es true, el servidor IIS establece el


HttpContext.User autenticado
mediante la autenticación de
Windows. Si es false , el servidor
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 obtener más información, vea
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.

Modelo de hospedaje fuera de proceso


Para configurar las opciones de IIS, incluya una configuración de servicio para IISOptions en
ConfigureServices. En el ejemplo siguiente se impide que la aplicación rellene
HttpContext.Connection.ClientCertificate :

services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = false;
});

OPCIÓN DEFAULT PARÁMETRO


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 middleware 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 configurar el módulo ASP.NET correctamente, 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. Si el archivo web.config está presente y el sitio se inicia normalmente, IIS no facilita
estos archivos confidenciales, en el caso de que se soliciten. 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.
Transformación de web.config
Si necesita transformar web.config al realizar la publicación (por ejemplo, establecer variables de entorno
basadas en la configuración, el perfil o el entorno), consulte Transformación de web.config.

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 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 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
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 permite que las aplicaciones ASP.NET Core se
ejecuten detrás de IIS. 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.

IMPORTANT
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.
Si el conjunto de hospedaje se instala después de hacer lo propio con la versión de 64 bits (x64) de .NET Core, es posible
que los SDK no estén disponibles (No se ha detectado ningún SDK de .NET Core). Para resolver el problema, consulte
Solución de problemas de proyectos de ASP.NET Core.

Descarga directa (versión actual)


Descargue al instalador mediante el vínculo siguiente:
Instalador del conjunto de hospedaje de .NET Core actual (descarga directa)
Versiones anteriores del instalador
Para obtener una versión anterior del instalador:
1. Vaya a los archivos de descarga de .NET.
2. En .NET Core, seleccione la versión de .NET Core.
3. En la columna Run apps - Runtime (Ejecutar aplicaciones - Runtime), busque la fila de la versión del
runtime de .NET Core que quiera instalar.
4. Descargue el instalador mediante el vínculo Runtime & Hosting Bundle (Runtime y conjunto de
hospedaje).

WARNING
Algunos instaladores contienen versiones que han alcanzado el final del ciclo de vida (EOL) y ya no son compatibles con
Microsoft. Para obtener más información, consulte la política de soporte técnico.

Instalación del conjunto de hospedaje


1. Ejecute el instalador en el servidor. Los parámetros siguientes están disponibles cuando se ejecuta el
instalador desde un shell de comandos de administrador:
OPT_NO_ANCM=1 – Omita la instalación del módulo de ASP.NET Core.
OPT_NO_RUNTIME=1 – Omita la instalación del entorno de ejecución de .NET Core.
OPT_NO_SHAREDFX=1 – Omita la instalación del marco compartido de ASP.NET (entorno de ejecución
de ASP.NET).
OPT_NO_X86=1 – Omita la instalación de entornos de ejecución x86. Utilice este parámetro cuando
sepa que no va a hospedar aplicaciones de 32 bits. Si hay alguna posibilidad de que vaya a hospedar
aplicaciones de 32 bits y 64 bits en el futuro, no use este parámetro e instale ambos entornos de
ejecución.
OPT_NO_SHARED_CONFIG_CHECK=1 – Deshabilite la comprobación para usar una configuración
compartida de IIS cuando la configuración compartida (applicationHost.config) esté en la misma
máquina que la instalación de IIS. Solo disponible para ASP.NET Core 2.2 o instaladores del conjunto
de hospedaje posteriores. Para más información, consulte Módulo ASP.NET Core.
2. Reinicie el sistema o ejecute net stop was /y, seguido de net start w3svc, desde un shell de comandos.
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. 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.
3. 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.

4. En el nodo del servidor, seleccione Grupos de aplicaciones.


5. Haga clic con el botón derecho en el grupo de aplicaciones del sitio y seleccione Configuración básica
en el menú contextual.
6. 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 (.NET CLR ); Core Common Language Runtime (CoreCLR ) para
.NET Core se arranca para hospedar la aplicación en el proceso de trabajo. El establecimiento de
Versión de .NET CLR en Sin código administrado es opcional, pero no es lo recomendable.
7. ASP.NET Core 2.2 o posterior: en el caso de las implementaciones independientes de 64 bits (x64) en las
que se use un modelo de hospedaje en proceso, deshabilite el grupo de aplicaciones de los procesos de
32 bits (x86).
En la barra lateral Acciones de la sección Grupos de aplicaciones de Administrador de IIS, seleccione
Establecer valores predeterminados de grupos de aplicaciones o Configuración avanzada.
Busque la opción Habilitar aplicaciones de 32 bits y establezca el valor en False . Las aplicaciones
implementadas para un hospedaje fuera de proceso no se ven afectadas por este ajuste.
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.
Utilice PowerShell para colocar app_offline.html (es necesario PowerShell 5 o una versión posterior):

$pathToApp = 'PATH_TO_APP'

# Stop the AppPool


New-Item -Path $pathToApp app_offline.htm

# Provide script commands here to deploy the app

# Restart the AppPool


Remove-Item -Path $pathToApp app_offline.htm

Protección de datos
La pila de protección de datos de ASP.NET Core la usan varios middlewares de ASP.NET Core, incluidos los
que se emplean en la autenticación. Aunque el código de usuario no llame a las API de protección de datos, la
protección de datos se debe configurar con un script de implementación o en el código de usuario para crear
un almacén de claves criptográficas persistente. Si no se configura la protección de datos, las claves se
conservan en memoria y se descartan cuando se reinicia la aplicación.
Si el conjunto de claves se almacena en memoria cuando se reinicia la aplicación:
Todos los tokens de autenticación basados en cookies se invalidan.
Los usuarios tienen que iniciar sesión de nuevo en la siguiente solicitud.
Ya no se pueden descifrar los datos protegidos con el conjunto de claves. Esto puede incluir tokens CSRF y
cookies de TempData de ASP.NET Core MVC.
Para configurar la protección de datos en IIS para conservar el conjunto de claves, use uno de los enfoques
siguientes:
Crear claves del Registro de protección de datos
Las claves de protección de datos que las aplicaciones de ASP.NET usan se almacenan en el Registro
externo a las aplicaciones. Para conservar las claves de una determinada aplicación, cree claves del
Registro para el grupo de aplicaciones.
En las instalaciones independientes de IIS que no son de granja de servidores web, puede usar el script
de PowerShell Provision-AutoGenKeys.ps1 de protección de datos para cada grupo de aplicaciones
usado con una aplicación de ASP.NET Core. Este script crea una clave del Registro en el registro HKLM
que solo es accesible a la cuenta de proceso de trabajo del grupo de aplicaciones de la aplicación. Las
claves se cifran en reposo mediante DPAPI con una clave de equipo.
En escenarios de granja de servidores web, una aplicación puede configurarse para usar una ruta de
acceso UNC para almacenar su conjunto de claves de protección de datos. De forma predeterminada,
las claves de protección de datos no se cifran. Asegúrese de que los permisos de archivo de un recurso
compartido de red se limitan a la cuenta de Windows bajo la que se ejecuta la aplicación. Puede usar un
certificado X509 para proteger las claves en reposo. Considere un mecanismo que permita a los
usuarios cargar certificados: coloque los certificados en el almacén de certificados de confianza del
usuario y asegúrese de que están disponibles en todos los equipos en los que se ejecuta la aplicación del
usuario. Vea Configurar la protección de datos en ASP.NET Core para más información.
Configurar el grupo de aplicaciones de IIS para cargar el perfil de usuario
Esta opción está en la sección Modelo de proceso, en la Configuración avanzada del grupo de
aplicaciones. Establezca Cargar perfil de usuario en True . Cuando se establece en True , las claves se
almacenan en el directorio del perfil de usuario y se protegen mediante DPAPI con una clave específica
de la cuenta de usuario. Las claves se conservan en la carpeta %HOME%\ASP.NET\DataProtection-
Keys.
También se debe habilitar el atributo setProfileEnvironment del grupo de aplicaciones. El valor
predeterminado de setProfileEnvironment es true . En algunos escenarios (por ejemplo, SO Windows),
setProfileEnvironment está establecido en false . Si las claves no se almacenan en el directorio del
perfil de usuario como se esperaba:
1. Vaya a la carpeta %windir%/system32/inetsrv/config.
2. Abra el archivo applicationHost.config.
3. Busque el elemento
<system.applicationHost><applicationPools><applicationPoolDefaults><processModel> .
4. Confirme que el atributo setProfileEnvironment no está presente, que adopta de forma
predeterminada el valor true , o establezca explícitamente el valor del atributo en true .
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. Para
obtener más información, vea Protección de datos de ASP.NET Core.

Directorios virtuales
Los directorios virtuales de IIS no son compatibles con aplicaciones ASP.NET Core. Una aplicación puede
hospedarse como subaplicación.

Subaplicaciones
Se puede hospedar una aplicación ASP.NET Core como una subaplicación IIS. La ruta de acceso de la
subaplicación se convierte en parte de la dirección URL de la aplicación raíz.
Una aplicación secundaria no debe 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=".\MyApp.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=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>

Los vínculos a los recursos estáticos dentro de la aplicación secundaria deben utilizar una notación de una tilde
con una barra diagonal ( ~/ ). La notación de tilde barra diagonal desencadena un asistente de etiquetas para
anteponer la ruta de acceso de la aplicación secundaria al vínculo relativo representado. Para una aplicación
secundaria en /subapp_path , una imagen vinculada con src="~/image.png" se representa como
src="/subapp_path/image.png" . El middleware de archivos estáticos de la aplicación raíz no procesa la solicitud
de archivo estático. La solicitud se procesa mediante el middleware de archivos estáticos de la aplicación
secundaria.
Si el atributo de un recurso estático se establece en una ruta de acceso absoluta (por ejemplo,
src
src="/image.png" ), el vínculo se representa sin la base de la ruta de acceso de la aplicación secundaria. El
middleware de archivos estáticos de la aplicación raíz intenta atender al recurso desde el web root, lo que
resulta en una respuesta 404 - No encontrado a menos que el recurso estático esté disponible desde la
aplicación raíz.
Para hospedar una aplicación ASP.NET Core como aplicación secundaria en otra aplicación ASP.NET Core:
1. Establezca un grupo de aplicaciones para la aplicación secundaria. Establezca Versión de .NET CLR en
Sin código administrado porque Core Common Language Runtime (CoreCLR ) para .NET Core se
arranca para hospedar la aplicación en el proceso de trabajo, no el CLR de escritorio (.NET CLR ).
2. Agregue el sitio raíz en el Administrador de IIS con la aplicación secundaria en una carpeta en el sitio
raíz.
3. Haga clic con el botón derecho en la carpeta de la aplicación secundaria en el Administrador de IIS y
seleccione Convertir en aplicación.
4. En el cuadro de diálogo Agregar aplicación, use el botón Seleccionar en Grupo de aplicaciones
para asignar el grupo de aplicaciones que ha creado para la aplicación secundaria. Seleccione Aceptar.
La asignación de un grupo de aplicaciones independiente de la aplicación secundaria es un requisito cuando se
utiliza el modelo de hospedaje en proceso.
Para más información sobre el modelo de hospedaje en proceso y cómo configurar el módulo de ASP.NET
Core, consulte Módulo ASP.NET Core y Módulo ASP.NET Core.

Configuración de IIS con web.config


En escenarios de IIS que son funcionales para aplicaciones ASP.NET Core con el módulo ASP.NET Core, la
configuración de IIS está influenciada por la sección <system.webServer> de web.config. Por ejemplo, la
configuración de IIS es funcional para la compresión dinámica. 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
deshabilitarlo para una aplicación ASP.NET Core.
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
El aislamiento de los grupos de aplicaciones se determinan mediante el modelo de hospedaje:
Hospedaje dentro de proceso: es necesario que las aplicaciones se ejecuten en grupos de aplicaciones
distintos.
Hospedaje fuera de proceso: 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 de IIS es un único grupo de aplicaciones
por aplicació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.
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.


Compatibilidad con HTTP/2
HTTP/2 es compatible con ASP.NET Core en los escenarios de implementación de IIS siguientes:
En proceso
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Conexión con TLS 1.2 o una versión posterior
Fuera de proceso
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Las conexiones de servidor perimetral de acceso público usan HTTP/2, pero la conexión de proxy
inverso al servidor de Kestrel usa HTTP/1.1.
Conexión con TLS 1.2 o una versión posterior
Para una implementación en proceso cuando se establece una conexión HTTP/2, HttpRequest.Protocol notifica
HTTP/2 . Para una implementación fuera de proceso cuando se establece una conexión HTTP/2,
HttpRequest.Protocol notifica HTTP/1.1 .
Para obtener más información sobre los modelos de hospedaje en proceso y fuera de proceso, consulte
Módulo ASP.NET Core.
HTTP/2 es compatible con las implementaciones fuera de proceso que cumplen los requisitos básicos
siguientes:
Windows Server 2016/Windows 10 o posterior; IIS 10 o posterior
Las conexiones de servidor perimetral de acceso público usan HTTP/2, pero la conexión de proxy inverso al
servidor de Kestrel usa HTTP/1.1.
Marco de destino: no es aplicable a las implementaciones fuera de proceso, ya que IIS controla
completamente la conexión HTTP/2.
Conexión con TLS 1.2 o una versión posterior
Si se establece una conexión HTTP/2, HttpRequest.Protocol notifica HTTP/1.1 .
HTTP/2 está habilitado de forma predeterminada. Las conexiones vuelven a HTTP/1.1 si no se establece una
conexión HTTP/2. Para más información sobre la configuración HTTP/2 con implementaciones de IIS, vea
HTTP/2 en IIS.

Solicitudes CORS de preflight


Esta sección solo se aplica a las aplicaciones de ASP.NET Core que tienen como destino .NET Framework.
Para una aplicación de ASP.NET Core que tiene como destino .NET Framework, las solicitudes OPTIONS no
se pasan a la aplicación de forma predeterminada en IIS. Para obtener información sobre cómo configurar los
controladores de IIS de la aplicación en web.config para pasar las solicitudes OPTIONS, consulte Habilitar
solicitudes entre orígenes en ASP.NET Web API 2: Cómo funciona la CORS.

Módulo de inicialización de aplicaciones y tiempo de espera de


inactividad
Cuando se hospeda en IIS mediante la versión 2 del módulo de ASP.NET Core:
Módulo de inicialización de aplicaciones – las aplicaciones hospedadas dentro de proceso o fuera de
proceso se pueden configurar para iniciarse de forma automática en un reinicio de proceso de trabajo o un
reinicio de servidor.
Tiempo de espera de inactividad: las aplicaciones hospedadas dentro de proceso se pueden configurar para
que no tengan tiempo de espera durante períodos de inactividad.
Módulo de inicialización de aplicaciones
Se aplica a las aplicaciones hospedadas dentro de proceso y fuera de proceso.
Inicialización de aplicaciones de IIS es una característica de IIS que envía una solicitud HTTP a la aplicación al
iniciarse o reciclarse el grupo de aplicaciones. La solicitud desencadena el inicio de la aplicación. De forma
predeterminada, IIS emite una solicitud a la dirección URL raíz de la aplicación ( / ) para inicializar esta
(consulte los recursos adicionales para más detalles sobre la configuración).
Confirme que la característica de rol Inicialización de aplicaciones de IIS está habilitada:
En Windows 7 o sistemas de escritorio posteriores cuando se usa IIS localmente:
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 Internet Information Services > Servicios World Wide Web > Características de desarrollo de
aplicaciones.
3. Active la casilla de Inicialización de aplicaciones.
En Windows Server 2008 R2 o posterior:
1. Abra el Asistente para agregar roles y características.
2. En el panel Seleccionar servicios de rol, abra el nodo Desarrollo de aplicaciones.
3. Active la casilla de Inicialización de aplicaciones.
Use cualquiera de los enfoques siguientes para habilitar el módulo de inicialización de aplicaciones para el
sitio:
Mediante el Administrador de IIS:
1. Seleccione Grupos de aplicaciones en el panel Conexiones.
2. Haga clic con el botón derecho en el grupo de aplicaciones de la aplicación en la lista y seleccione
Configuración avanzada.
3. El valor predeterminado de Modo de inicio es OnDemand. Establezca Modo de inicio en
AlwaysRunning. Seleccione Aceptar.
4. Abra el nodo Sitios en el panel Conexiones.
5. Haga clic con el botón derecho en la aplicación y seleccione Administrar sitio web >
Configuración avanzada.
6. El valor predeterminado de Carga previa activada es False. Establezca Carga previa activada en
True. Seleccione Aceptar.
Mediante web.config, agregue el elemento <applicationInitialization> con doAppInitAfterRestart
establecido en true a los elementos <system.webServer> del archivo web.config de la aplicación:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<applicationInitialization doAppInitAfterRestart="true" />
</system.webServer>
</location>
</configuration>

Tiempo de espera de inactividad


Solo se aplica a las aplicaciones hospedadas dentro de proceso.
Para evitar la inactividad en la aplicación, establezca el tiempo de espera de inactividad del grupo de
aplicaciones mediante el Administrador de IIS:
1. Seleccione Grupos de aplicaciones en el panel Conexiones.
2. Haga clic con el botón derecho en el grupo de aplicaciones de la aplicación en la lista y seleccione
Configuración avanzada.
3. El valor predeterminado de Tiempo de inactividad (minutos) es 20 minutos. Establezca Tiempo de
inactividad (minutos) en 0 (cero). Seleccione Aceptar.
4. Desactive y vuelva a activar el proceso de trabajo.
Para evitar que las aplicaciones hospedadas fuera de proceso agoten el tiempo de espera, use cualquiera de los
enfoques siguientes:
Haga ping a la aplicación desde un servicio externo con el fin de mantenerla funcionando.
Si la aplicación solo hospeda servicios en segundo plano, evite el hospedaje de IIS y use un servicio de
Windows para hospedar la aplicación de ASP.NET Core.
Recursos adicionales del módulo de inicialización de aplicaciones y del tiempo de espera de inactividad
Inicialización de aplicaciones IIS 8.0
Inicialización de la aplicación <applicationInitialization >.
Configuración del modelo de proceso para un grupo de aplicaciones <processModel >.

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
Solución de problemas de proyectos de ASP.NET Core
Introducción a ASP.NET Core
Sitio oficial de Microsoft IIS
Biblioteca de contenido técnico de Windows Server
HTTP/2 en IIS
Transformación de web.config
Módulo ASP.NET Core
03/07/2019 • 42 minutes to read • Edit Online

Por Tom Dykstra, Rick Strahl, Chris Ross, Rick Anderson, Sourabh Shirhatti, Justin Kotalik y
Luke Latham
El módulo ASP.NET Core es un módulo nativo de IIS que se conecta a la canalización de IIS
para:
Hospedar una aplicación ASP.NET Core dentro del proceso de trabajo de IIS ( w3wp.exe ), lo
que se denomina modelo de hospedaje en proceso.
Reenviar solicitudes web a una aplicación ASP.NET Core de back-end que ejecuta el
servidor de Kestrel, lo que se denomina modelo de hospedaje fuera de proceso.
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
En el caso del hospedaje en proceso, el módulo usa el servidor HTTP de IIS ( IISHttpServer ),
una implementación de servidor en proceso de IIS.
Cuando se hospeda fuera de proceso, el módulo solo funciona con Kestrel. El módulo no es
compatible con HTTP.sys.

Modelos de hospedaje
Modelo de hospedaje en proceso
Para configurar una aplicación para el hospedaje en proceso, agregue la propiedad
<AspNetCoreHostingModel> al archivo de proyecto de la aplicación con un valor de InProcess (el
hospedaje fuera de proceso se establece con OutOfProcess ):

<PropertyGroup>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>

No se admite el modelo de hospedaje en proceso para aplicaciones de ASP.NET Core que


tienen como destino .NET Framework.
Si la propiedad <AspNetCoreHostingModel> no está presente en el archivo, el valor
predeterminado es OutOfProcess .
Al hospedar en proceso, se aplican las siguientes características:
En lugar del servidor HTTP de IIS ( IISHttpServer ) se usa el servidor Kestrel. En el
mismo proceso, CreateDefaultBuilder llama a UseIIS para:
Registrar el IISHttpServer .
Configurar el puerto y la ruta de acceso base donde debe escuchar el servidor al
ejecutarse detrás del módulo de ASP.NET Core.
Configurar el host para capturar errores de inicio.
El atributo requestTimeout no se aplica al hospedaje en proceso.
No se admite el uso compartido de un grupo de aplicaciones entre aplicaciones. Se usa
un grupo de aplicaciones por aplicación.
Cuando se usa Web Deploy o se coloca manualmente un archivo app_offline.htm en la
implementación, puede que la aplicación no se apague inmediatamente si hay una
conexión abierta. Por ejemplo, una conexión WebSocket puede retrasar el apagado de la
aplicación.
La arquitectura (valor de bits) de la aplicación y el runtime instalado (x64 o x86) deben
coincidir con la arquitectura del grupo de aplicaciones.
Si se configura el host de la aplicación manualmente con WebHostBuilder (no mediante
CreateDefaultBuilder) y la aplicación nunca se ejecuta directamente en el servidor de
Kestrel (autohospedada), llame a UseKestrel antes de llamar a UseIISIntegration . Si se
invierte el orden, el host no se inicia.
Se detectan las desconexiones del cliente. El token de cancelación
HttpContext.RequestAborted se cancela cuando el cliente se desconecta.
En ASP.NET Core 2.2.1 o versiones anteriores, GetCurrentDirectory devuelve el
directorio de trabajo del proceso iniciado por IIS en lugar del de la aplicación (por
ejemplo, C:\Windows\System32\inetsrv para w3wp.exe).
Para conocer el código de ejemplo que establece el directorio actual de la aplicación,
consulte la información sobre la clase CurrentDirectoryHelpers. Llame al método
SetCurrentDirectory . Las llamadas subsiguientes a GetCurrentDirectory proporcionan el
directorio de la aplicación.
Cuando se hospeda en el proceso, no se llama a AuthenticateAsync de forma interna
para inicializar un usuario. Por tanto, se usa una implementación de
IClaimsTransformation para transformar las notificaciones después de que cada
autenticación no se active de forma predeterminada. Al transformar notificaciones con
una implementación de IClaimsTransformation, llame a AddAuthentication para agregar
servicios de autenticación:

public void ConfigureServices(IServiceCollection services)


{
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
services.AddAuthentication(IISServerDefaults.AuthenticationScheme);
}

public void Configure(IApplicationBuilder app)


{
app.UseAuthentication();
}

Modelo de hospedaje fuera de proceso


Para configurar una aplicación para el hospedaje fuera de proceso, use uno de los métodos
siguientes en el archivo de proyecto:
No especifique la propiedad <AspNetCoreHostingModel> . Si la propiedad
<AspNetCoreHostingModel> no está presente en el archivo, el valor predeterminado es
OutOfProcess .
Establezca el valor de la propiedad <AspNetCoreHostingModel> en OutOfProcess (el hospedaje
en proceso se establece con InProcess ):
<PropertyGroup>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>

En lugar del servidor Kestrel se usa el servidor HTTP de IIS ( IISHttpServer ).


Fuera del proceso, CreateDefaultBuilder llama a UseIISIntegration para:
Configurar el puerto y la ruta de acceso base donde debe escuchar el servidor al ejecutarse
detrás del módulo de ASP.NET Core.
Configurar el host para capturar errores de inicio.
Cambios del modelo de hospedaje
Si se modifica el valor hostingModel en el archivo web.config (se explica en la sección
Configuración con web.config), el módulo recicla el proceso de trabajo de IIS.
En IIS Express, el módulo no recicla el proceso de trabajo, sino que desencadena un cierre
estable del proceso de IIS Express actual. La siguiente solicitud a la aplicación genera un nuevo
proceso de IIS Express.
Nombre del proceso
Process.GetCurrentProcess().ProcessName informa a w3wp / iisexpress (en proceso) o dotnet
(fuera de proceso).
El módulo ASP.NET Core es un módulo nativo de IIS que se conecta a la canalización de IIS
para reenviar solicitudes web a aplicaciones ASP.NET Core de back-end.
Versiones de Windows compatibles:
Windows 7 o posterior
Windows Server 2008 R2 o posterior
El módulo funciona únicamente con Kestrel. El módulo no es compatible con HTTP.sys.
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 IIS, el módulo ASP.NET Core y una
aplicación:

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 la 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. El middleware agregado por la integración
de IIS actualiza el esquema, la dirección IP remota y PathBase para responder del reenvío de la
solicitud a Kestrel. 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.
Muchos de los módulos nativos, como la autenticación de Windows, permanecen activos. Para
obtener más información sobre los módulos de IIS activos con el módulo ASP.NET Core, vea
Módulos de IIS con ASP.NET Core.
El módulo ASP.NET Core también 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 sobre cómo instalar y usar el módulo ASP.NET Core, consulte
Instalación del conjunto de hospedaje de .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>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess" />
</system.webServer>
</location>
</configuration>
<?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>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\MyApp.exe"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="InProcess" />
</system.webServer>
</location>
</configuration>

La propiedad InheritInChildApplications está establecida en false para indicar que las


aplicaciones que residen en un subdirectorio de la aplicación no heredan la configuración
especificada en el elemento <location>.

<?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.
Para obtener información sobre la configuración de aplicaciones secundarias de IIS, consulte
Hospedaje de ASP.NET Core en Windows con IIS.
Atributos del elemento aspNetCore
ATRIBUTO DESCRIPCIÓN DEFAULT

arguments Atributo de cadena opcional.


Argumentos para el archivo
ejecutable especificado en
processPath.

disableStartUpErrorPage Atributo Boolean opcional. 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 Atributo Boolean opcional. 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.

hostingModel Atributo de cadena opcional. OutOfProcess

Especifica el modelo de
hospedaje como en proceso
( InProcess ) o fuera de
proceso ( OutOfProcess ).

processesPerApplication Atributo integer opcional. Valor predeterminado: 1


Mínimo: 1
Especifica el número de Máximo: 100 †
instancias del proceso
especificado en el valor
processPath que pueden
rotarse por aplicación.
†En el hospedaje en proceso,
el valor está limitado a 1 .
No se recomienda establecer
processesPerApplication .
Este atributo se quitará en
futuras versiones.
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. Valor predeterminado: 10


Mínimo: 0
Especifica el número de Máximo: 100
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.
No admitido con el
hospedaje en proceso.

requestTimeout Atributo timespan opcional. Valor predeterminado:


00:02:00
Especifica el tiempo que el Mínimo: 00:00:00
módulo ASP.NET Core Máximo: 360:00:00
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.
No se aplica al hospedaje en
proceso. En el hospedaje en
proceso, el módulo espera a
que la aplicación procese la
solicitud.
Los valores válidos para los
segmentos de minutos y
segundos de la cadena se
encuentran en el rango 0-
59. El uso de 60 en el valor
de minutos o segundos da
como resultado el error 500:
Error interno del servidor.
ATRIBUTO DESCRIPCIÓN DEFAULT

shutdownTimeLimit Atributo integer opcional. Valor predeterminado: 10


Mínimo: 0
Tiempo en segundos que el Máximo: 600
módulo espera a que se
cierre correctamente el
archivo ejecutable cuando se
detecta el archivo
app_offline.htm.

startupTimeLimit Atributo integer opcional. Valor predeterminado: 120


Mínimo: 0
Tiempo en segundos que Máximo: 3600
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.
Un valor de 0 (cero) no se
considera un tiempo de
expiración infinito.

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. Al crearse el
archivo de registro, el
módulo crea las carpetas
que se proporcionan en la
ruta de acceso. 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_1
934.log 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 Atributo Boolean opcional. 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.
ATRIBUTO DESCRIPCIÓN DEFAULT

forwardWindowsAuthToken Atributo Boolean opcional. 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.

processesPerApplication Atributo integer opcional. Valor predeterminado: 1


Mínimo: 1
Especifica el número de Máximo: 100
instancias del proceso
especificado en el valor
processPath que pueden
rotarse por aplicación.
No se recomienda establecer
processesPerApplication .
Este atributo se quitará en
futuras versiones.

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. Valor predeterminado: 10


Mínimo: 0
Especifica el número de Máximo: 100
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. Valor predeterminado:


00:02:00
Especifica el tiempo que el Mínimo: 00:00:00
módulo ASP.NET Core Máximo: 360:00:00
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. Valor predeterminado: 10


Mínimo: 0
Tiempo en segundos que el Máximo: 600
módulo espera a que se
cierre correctamente el
archivo ejecutable cuando se
detecta el archivo
app_offline.htm.

startupTimeLimit Atributo integer opcional. Valor predeterminado: 120


Mínimo: 0
Tiempo en segundos que Máximo: 3600
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.
Un valor de 0 (cero) no se
considera un tiempo de
expiración infinito.

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_1
934.log 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.
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> .

WARNING
Las variables de entorno establecidas en esta sección entran en conflicto con las variables de entorno
del sistema que tienen el mismo nombre. Si se establece una variable de entorno en el archivo
web.config y en el nivel del sistema en Windows, el valor del archivo web.config se anexa al valor de la
variable de entorno del sistema (por ejemplo, ASPNETCORE_ENVIRONMENT: Development;Development ),
que impide que se inicie la aplicación.
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"
hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="CONFIG_DIR" value="f:\application_config" />
</environmentVariables>
</aspNetCore>

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

NOTE
Una alternativa a establecer directamente el entorno en web.config consiste en incluir la propiedad
<EnvironmentName> en el perfil de publicación ( .pubxml) o el archivo de proyecto. Este método
establece el entorno en web.config cuando se publica el proyecto:

<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

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.
Al usar el modelo de hospedaje fuera de proceso, puede que la aplicación no se apague
inmediatamente si hay una conexión abierta. Por ejemplo, una conexión WebSocket puede
retrasar el apagado de la aplicación.

Página de errores de inicio


Tanto el hospedaje en proceso como el hospedaje fuera de proceso generan páginas de error
personalizado cuando se produce un error al iniciar la aplicación.
Si el módulo ASP.NET Core no logra encontrar el controlador de solicitudes en proceso o fuera
de proceso, aparecerá la página de código de estado 500.0 - Error de carga de controlador en
proceso/fuera de proceso.
Para el hospedaje en proceso, si el módulo ASP.NET Core no logra iniciar la aplicación,
aparecerá la página de código de estado 500.30 - Error de inicio.
En cuanto al hospedaje fuera de proceso, 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,
aparecerá la 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 5xx de IIS predeterminada,
use el atributo disableStartUpErrorPage . Para obtener más información sobre cómo configurar
los mensajes de error personalizados, consulte Errores HTTP <httpErrors>.
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 obtener más información sobre
cómo configurar los 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 . Al
crearse el archivo de registro, el módulo crea las carpetas de la ruta de acceso de
stdoutLogFile . 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.
Si stdoutLogEnabled es falso, los errores que se produzcan al iniciar la aplicación se registrarán
y se emitirán en el registro de eventos hasta un máximo de 30 KB. Después del inicio, se
descartarán los registros adicionales.
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"
hostingModel="InProcess">
</aspNetCore>

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
</aspNetCore>

Registros de diagnóstico mejorados


El módulo ASP.NET Core es configurable para proporcionar registros de diagnóstico
mejorados. Agregue el elemento <handlerSettings> al elemento <aspNetCore> de web.config.
Al establecer debugLevel en TRACE se expone una fidelidad mayor de información de
diagnóstico:
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout"
hostingModel="InProcess">
<handlerSettings>
<handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
<handlerSetting name="debugLevel" value="FILE,TRACE" />
</handlerSettings>
</aspNetCore>

Al crearse el archivo de registro, el módulo crea las carpetas de la ruta de acceso (logs en el
ejemplo anterior). 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).
El módulo no crea automáticamente las carpetas de la ruta de acceso proporcionada al valor
<handlerSetting> (logs en el ejemplo anterior ), que deben existir previamente en la
implementación. 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 valores de nivel de depuración ( debugLevel ) pueden incluir el nivel y la ubicación.
Niveles (en orden de menos a más detallado):
ERROR
WARNING
INFO
TRACE
Ubicaciones (se permiten varias ubicaciones):
CONSOLE
EVENTLOG
ARCHIVO
También se puede proporcionar la configuración de controlador a través de variables de
entorno:
ASPNETCORE_MODULE_DEBUG_FILE – Ruta de acceso al archivo de registro de depuración. (El
valor predeterminado es aspnetcore-debug.log)
ASPNETCORE_MODULE_DEBUG – Valor de nivel de depuración.

WARNING
No deje habilitado el registro de depuración más tiempo del necesario en la implementación para
solucionar un problema. El tamaño del registro no es limitado. Dejar habilitado el registro de
depuración puede agotar el espacio disponible en disco y bloquear el servidor o el servicio de
aplicación.

Consulte Configuración con web.config para ver un ejemplo del elemento aspNetCore en el
archivo web.config.

Modificación del tamaño de la pila


Configure el tamaño de la pila administrada mediante el valor stackSize en bytes. El tamaño
predeterminado es 1048576 bytes (1 MB ).

<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout"
hostingModel="InProcess">
<handlerSettings>
<handlerSetting name="stackSize" value="2097152" />
</handlerSettings>
</aspNetCore>

La configuración de proxy usa el protocolo HTTP y un


token de emparejamiento
Solo se aplica al hospedaje fuera de proceso.
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 ( MS-ASPNETCORE-TOKEN ) 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
TrustedInstaller. Como 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 inicia un error de acceso denegado al intentar configurar los valores del módulo en el
archivo applicationHost.config del recurso compartido.
Cuando se usa una configuración compartida de IIS en el mismo equipo que la instalación de
IIS, ejecute el instalador del lote de hospedaje de ASP.NET Core con el parámetro
OPT_NO_SHARED_CONFIG_CHECK establecido en 1 :

dotnet-hosting-{VERSION}.exe OPT_NO_SHARED_CONFIG_CHECK=1

Cuando la ruta de acceso a la configuración compartida no se encuentra en el mismo equipo


que la instalación 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.
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
%ProgramFiles%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll
%ProgramFiles(x86)%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll
IIS Express (x86/amd64):
%ProgramFiles%\IIS Express\aspnetcore.dll
%ProgramFiles(x86)%\IIS Express\aspnetcore.dll
%ProgramFiles%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll
%ProgramFiles(x86)%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll
Schema
IIS
%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml
%windir%\System32\inetsrv\config\schema\aspnetcore_schema_v2.xml
IIS Express
%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml
%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema_v2.xml
Configuración
IIS
%windir%\System32\inetsrv\config\applicationHost.config
IIS Express
Visual Studio: {APPLICATION ROOT}\.vs\config\applicationHost.config
iisexpress.exe CLI:
%USERPROFILE%\Documents\IISExpress\config\applicationhost.config
Los archivos se pueden encontrar mediante la búsqueda de aspnetcore en el archivo
applicationHost.config.

Recursos adicionales
Hospedaje de ASP.NET Core en Windows con IIS
Repositorio GitHub del módulo ASP.NET Core (origen de referencia)
Módulos de IIS con ASP.NET Core
Compatibilidad de IIS de tiempo de desarrollo en
Visual Studio para ASP.NET Core
10/05/2019 • 11 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 con IIS en Windows Server. En este tema se explica cómo habilitar este escenario 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 (para compatibilidad con HTTPS )

Habilitar IIS
1. En Windows, 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. Seleccione Aceptar.
La instalación de IIS puede requerir un reinicio del sistema.

Configurar IIS
IIS debe tener un sitio web configurado con lo siguiente:
Nombre de host: por lo general, el Sitio web predeterminado se usa con un Nombre de host de
localhost . Sin embargo, sirve cualquier sitio web de IIS válido con un nombre de host único.
Enlace de sitio
Para las aplicaciones que requieran HTTPS, cree un enlace al puerto 443 con un certificado. Por lo
general, se usa el certificado de desarrollo de IIS Express, pero cualquier certificado válido sirve.
Para las aplicaciones que usan HTTP, confirme la existencia de un enlace al puerto 80 o cree un enlace a
dicho puerto si se trata de un sitio nuevo.
Utilice un enlace único para HTTP o HTTPS. No se admite el enlace al mismo tiempo a los puertos
HTTP y HTTPS.

Habilitación de la compatibilidad con IIS en tiempo de desarrollo en


Visual Studio
1. Inicie el instalador de Visual Studio.
2. Seleccione Modificar para la instalación de Visual Studio que se pretende usar para la compatibilidad con
IIS en tiempo de desarrollo.
3. Para la carga de trabajo de Desarrollo de ASP.NET y web , busque e instale el componente
Compatibilidad con IIS en tiempo de desarrollo.
El componente se enumera en la sección Opcional, en Compatibilidad con IIS en tiempo de desarrollo,
dentro del panel Detalles de instalación a la derecha de las cargas de trabajo. El componente instala el
módulo ASP.NET Core, que es un módulo de IIS nativo necesario para ejecutar aplicaciones de ASP.NET
Core con IIS.

Configuración del proyecto


Redireccionamiento de HTTPS
Para un nuevo proyecto que requiere HTTPS, seleccione la casilla Configurar para HTTPS en la ventana Crear
una aplicación web ASP.NET Core. Al seleccionar la casilla, se agrega redireccionamiento HTTPS y middleware
HSTS a la aplicación cuando esta se crea.
Para un proyecto existente que requiere HTTPS, use el redireccionamiento HTTPS y el middleware HSTS en
Startup.Configure . Para obtener más información, vea Exigir HTTPS en ASP.NET Core.

Para un proyecto que usa HTTP, el redireccionamiento HTTPS y el middleware HSTS no se agregan a la aplicación.
No se requiere ninguna configuración de la aplicación.
Perfil de inicio de IIS
Cree un nuevo perfil de inicio para agregar la compatibilidad con IIS en tiempo de desarrollo:
1. Haga clic con el botón derecho en el Explorador de soluciones. Haga clic en Propiedades. Abra la
pestaña Depurar.
2. En Perfil, seleccione el botón Nuevo. Asigne el perfil el nombre "IIS" en la ventana emergente. Seleccione
Aceptar para crear el perfil.
3. En Iniciar, seleccione IIS en la lista.
4. Active la casilla Iniciar explorador y proporcione la dirección URL del punto de conexión.
Si la aplicación requiere HTTPS, use un punto de conexión HTTPS ( https:// ). Para HTTP, use un punto de
conexión HTTP ( http:// ).
Proporcione el mismo nombre de host y puerto especificados en la configuración de IIS establecida en usos
anteriores, normalmente localhost .
Proporcione el nombre de la aplicación al final de la dirección URL.
Por ejemplo, https://localhost/WebApplication1 (HTTPS ) o http://localhost/WebApplication1 (HTTP ) son
direcciones URL de punto de conexión válidas.
5. En la sección Variables de entorno, seleccione el botón Agregar. Proporcione una variable de entorno con
un Nombre de ASPNETCORE_ENVIRONMENT y un Valor de Development .
6. En el área Configuración del servidor web, defina la Dirección URL de la aplicación con el mismo
valor utilizado para la dirección URL de punto de conexión de Iniciar el explorador.
7. Para la configuración del Modelo de hospedaje de Visual Studio 2019 o posterior, seleccione
Predeterminado para usar el modelo de hospedaje utilizado por el proyecto. Si el proyecto establece la
propiedad <AspNetCoreHostingModel> en su archivo del proyecto, se usa el valor de la propiedad ( InProcess
o OutOfProcess ). Si la propiedad no existe, se usa el modelo de hospedaje predeterminado de la aplicación,
que está en proceso. Si la aplicación requiere una configuración del modelo de hospedaje distinta a la del
modelo de hospedaje habitual de la aplicación, defina el Modelo de hospedaje como In Process o
Out Of Process , según proceda.

8. Guarde el perfil.
1. Haga clic con el botón derecho en el Explorador de soluciones. Haga clic en Propiedades. Abra la
pestaña Depurar.
2. En Perfil, seleccione el botón Nuevo. Asigne el perfil el nombre "IIS" en la ventana emergente. Seleccione
Aceptar para crear el perfil.
3. En Iniciar, seleccione IIS en la lista.
4. Active la casilla Iniciar explorador y proporcione la dirección URL del punto de conexión.
Si la aplicación requiere HTTPS, use un punto de conexión HTTPS ( https:// ). Para HTTP, use un punto de
conexión HTTP ( http:// ).
Proporcione el mismo nombre de host y puerto especificados en la configuración de IIS establecida en usos
anteriores, normalmente localhost .
Proporcione el nombre de la aplicación al final de la dirección URL.
Por ejemplo, https://localhost/WebApplication1 (HTTPS ) o http://localhost/WebApplication1 (HTTP ) son
direcciones URL de punto de conexión válidas.
5. En la sección Variables de entorno, seleccione el botón Agregar. Proporcione una variable de entorno con
un Nombre de ASPNETCORE_ENVIRONMENT y un Valor de Development .
6. En el área Configuración del servidor web, defina la Dirección URL de la aplicación con el mismo
valor utilizado para la dirección URL de punto de conexión de Iniciar el explorador.
7. Para la configuración del Modelo de hospedaje de Visual Studio 2019 o posterior, seleccione
Predeterminado para usar el modelo de hospedaje utilizado por el proyecto. Si el proyecto establece la
propiedad <AspNetCoreHostingModel> en su archivo del proyecto, se usa el valor de la propiedad ( InProcess
o OutOfProcess ). Si la propiedad no existe, se usa el modelo de hospedaje predeterminado de la aplicación,
que está fuera de proceso. Si la aplicación requiere una configuración del modelo de hospedaje distinta a la
del modelo de hospedaje habitual de la aplicación, defina el Modelo de hospedaje como In Process o
Out Of Process , según proceda.

8. Guarde el perfil.
Si no se usa Visual Studio, agregue manualmente un perfil de inicio al archivo launchSettings.json en la carpeta
Propiedades. El siguiente ejemplo configura el perfil para usar el protocolo HTTPS:

{
"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"
}
}
}
}
Confirme que los puntos de conexión applicationUrl y launchUrl coinciden y use el mismo protocolo que la
configuración de enlace de IIS, es decir, HTTP o HTTPS.

Ejecución del proyecto


Ejecute Visual Studio como administrador:
Confirme que la lista desplegable de configuración de compilación está configurada como Depurar.
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.

NOTE
La depuración de una configuración de compilación de versión con Solo mi código y las optimizaciones del compilador
degradan el rendimiento. Por ejemplo, no se alcanzan los puntos de interrupción.

Recursos adicionales
Introducción al Administrador de IIS en IIS
Hospedaje de ASP.NET Core en Windows con IIS
Introducción al módulo ASP.NET Core
Referencia de configuración del módulo ASP.NET Core
Aplicación de HTTPS
Módulos de IIS con ASP.NET Core
22/05/2019 • 12 minutes to read • Edit Online

Por Luke Latham


Algunos de los módulos nativos de IIS y todos los módulos administrados de IIS no pueden procesar las
solicitudes para las aplicaciones ASP.NET Core. En muchos casos, ASP.NET Core ofrece una alternativa a los
escenarios abordados por 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 aplicaciones ASP.NET Core y el
módulo 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

Seguimiento de solicitudes con Sí Registro de ASP.NET Core


error
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

Redireccionamiento HTTP Sí Middleware de reescritura de dirección


HttpRedirectionModule URL

Seguimiento de HTTP Sí
TracingModule

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 dirección 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
FUNCIONAL CON APLICACIONES ASP.NET
MODULE CORE OPCIÓN DE ASP.NET CORE

Contenido estático No Middleware de archivos estáticos


StaticFileModule

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


MODULE OPCIÓN 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, desbloquee primero el módulo y 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. Si la entrada de acción para el módulo
aparece como Bloqueo, el módulo ya está desbloqueado y no se requiere ninguna acción. 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, implemente 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. Si la entrada de acción para la sección del módulo aparece como
Sección de bloqueo, la sección del módulo ya está desbloqueada y no se requiere ninguna acción.
4. Agregue una sección <modules> al archivo local web.config de la aplicación con un elemento <remove>
para quitar el módulo de la aplicación. Agregue 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 según este enfoque no afecta al uso del
módulo con otras aplicaciones del servidor.

<configuration>
<system.webServer>
<modules>
<remove name="MODULE_NAME" />
</modules>
</system.webServer>
</configuration>

Para agregar o quitar módulos para IIS Express mediante web.config, modifique applicationHost.config para
desbloquear la sección <modules> :
1. Abra {APPLICATION ROOT }\.vs\config\applicationhost.config.
2. Localice el elemento <section> para los módulos de IIS y cambie overrideModeDefault de Deny a
Allow :

<section name="modules"
allowDefinition="MachineToApplication"
overrideModeDefault="Allow" />

3. Localice la sección <location path="" overrideMode="Allow"><system.webServer><modules> . Para los


módulos que desee quitar, establezca lockItem entre true y false . En el ejemplo siguiente, se
desbloquea el módulo CGI:

<add name="CgiModule" lockItem="false" />

4. Una vez la sección <modules> y los módulos individuales se desbloquean, tiene la opción de agregar o
quitar los módulos de IIS mediante el archivo web.config de la aplicación para ejecutar la aplicación en
IIS Express.
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 de ASP.NET Core 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>
Solución de problemas de ASP.NET Core en IIS
02/07/2019 • 21 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.
Temas adicionales de solución de problemas:
Azure App Service también usa el módulo de ASP.NET Core e IIS para hospedar aplicaciones. Para ver
consejos de solución de problemas pertenecientes específicamente a Azure App Service, consulte
Solución de problemas de ASP.NET Core en Azure App Service.
En Controlar errores en ASP.NET Core se explica cómo controlar los errores de aplicaciones de ASP.NET
Core durante el desarrollo en un sistema local.
En Información sobre cómo depurar con Visual Studio se presentan las características del depurador de
Visual Studio.
En Depuración con Visual Studio Code se describe la compatibilidad de depuración integrada en Visual
Studio Code.

App startup errors


In Visual Studio, an ASP.NET Core project defaults to IIS Express hosting during debugging. A 502.5 -
Process Failure or a 500.30 - Start Failure that occurs when debugging locally can be troubleshooted using
the advice in this topic.
In Visual Studio, an ASP.NET Core project defaults to IIS Express hosting during debugging. A 502.5 Process
Failure that occurs when debugging locally can be troubleshooted using the advice in this topic.
500 Internal Server Error
The app starts, but an error prevents the server from fulfilling the request.
This error occurs within the app's code during startup or while creating a response. The response may
contain no content, or the response may appear as a 500 Internal Server Error in the browser. The
Application Event Log usually states that the app started normally. From the server's perspective, that's
correct. The app did start, but it can't generate a valid response. Run the app at a command prompt on the
server or enable the ASP.NET Core Module stdout log to troubleshoot the problem.
500.0 In-Process Handler Load Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module fails to find the .NET Core CLR and find the in-process request handler
(aspnetcorev2_inprocess.dll). Check that:
The app targets either the Microsoft.AspNetCore.Server.IIS NuGet package or the
Microsoft.AspNetCore.App metapackage.
The version of the ASP.NET Core shared framework that the app targets is installed on the target
machine.
500.0 Out-Of-Process Handler Load Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module fails to find the out-of-process hosting request handler. Make sure the
aspnetcorev2_outofprocess.dll is present in a subfolder next to aspnetcorev2.dll.
500.0 In-Process Handler Load Failure
The worker process fails. The app doesn't start.
An unknown error occurred loading ASP.NET Core Module components. Take one of the following actions:
Contact Microsoft Support (select Developer Tools then ASP.NET Core).
Ask a question on Stack Overflow.
File an issue on our GitHub repository.
500.30 In-Process Startup Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the .NET Core CLR in-process, but it fails to start. The cause of
a process startup failure can usually be determined from entries in the Application Event Log and the
ASP.NET Core Module stdout log.
A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core
shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are
installed on the target machine.
500.31 ANCM Failed to Find Native Dependencies
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the .NET Core runtime in-process, but it fails to start. The most
common cause of this startup failure is when the Microsoft.NETCore.App or Microsoft.AspNetCore.App
runtime isn't installed. If the app is deployed to target ASP.NET Core 3.0 and that version doesn't exist on the
machine, this error occurs. An example error message follows:

The specified framework 'Microsoft.NETCore.App', version '3.0.0' was not found.


- The following frameworks were found:
2.2.1 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview5-27626-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27713-13 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27714-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
3.0.0-preview6-27723-08 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]

The error message lists all the installed .NET Core versions and the version requested by the app. To fix this
error, either:
Install the appropriate version of .NET Core on the machine.
Change the app to target a version of .NET Core that's present on the machine.
Publish the app as a self-contained deployment.
When running in development (the ASPNETCORE_ENVIRONMENT environment variable is set to Development ), the
specific error is written to the HTTP response. The cause of a process startup failure is also found in the
Application Event Log.
500.32 ANCM Failed to Load dll
The worker process fails. The app doesn't start.
The most common cause for this error is that the app is published for an incompatible processor architecture.
If the worker process is running as a 32-bit app and the app was published to target 64-bit, this error occurs.
To fix this error, either:
Republish the app for the same processor architecture as the worker process.
Publish the app as a framework-dependent deployment.
500.33 ANCM Request Handler Load Failure
The worker process fails. The app doesn't start.
The app didn't reference the Microsoft.AspNetCore.App framework. Only apps targeting the
Microsoft.AspNetCore.App framework can be hosted by the ASP.NET Core Module.

To fix this error, confirm that the app is targeting the Microsoft.AspNetCore.App framework. Check the
.runtimeconfig.json to verify the framework targeted by the app.

500.34 ANCM Mixed Hosting Models Not Supported


The worker process can't run both an in-process app and an out-of-process app in the same process.
To fix this error, run apps in separate IIS application pools.
500.35 ANCM Multiple In-Process Applications in same Process
The worker process can't run both an in-process app and an out-of-process app in the same process.
To fix this error, run apps in separate IIS application pools.
500.36 ANCM Out-Of-Process Handler Load Failure
The out-of-process request handler, aspnetcorev2_outofprocess.dll, isn't next to the aspnetcorev2.dll file. This
indicates a corrupted installation of the ASP.NET Core Module.
To fix this error, repair the installation of the .NET Core Hosting Bundle (for IIS ) or Visual Studio (for IIS
Express).
500.37 ANCM Failed to Start Within Startup Time Limit
ANCM failed to start within the provied startup time limit. By default, the timeout is 120 seconds.
This error can occur when starting a large number of apps on the same machine. Check for CPU/Memory
usage spikes on the server during startup. You may need to stagger the startup process of multiple apps.
502.5 Process Failure
The worker process fails. The app doesn't start.
The ASP.NET Core Module attempts to start the worker process but it fails to start. The cause of a process
startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core
Module stdout log.
A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core
shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are
installed on the target machine. The shared framework is the set of assemblies (.dll files) that are installed on
the machine and referenced by a metapackage such as Microsoft.AspNetCore.App . The metapackage
reference can specify a minimum required version. For more information, see The shared framework.
The 502.5 Process Failure error page is returned when a hosting or app misconfiguration causes the worker
process to fail:
Failed to start application (ErrorCode '0x800700c1')

EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.

The app failed to start because the app's assembly (.dll) couldn't be loaded.
This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress
process.
Confirm that the app pool's 32-bit setting is correct:
1. Select the app pool in IIS Manager's Application Pools.
2. Select Advanced Settings under Edit Application Pool in the Actions panel.
3. Set Enable 32-Bit Applications:
If deploying a 32-bit (x86) app, set the value to True .
If deploying a 64-bit (x64) app, set the value to False .
Connection reset
If an error occurs after the headers are sent, it's too late for the server to send a 500 Internal Server Error
when an error occurs. This often happens when an error occurs during the serialization of complex objects for
a response. This type of error appears as a connection reset error on the client. Application logging can help
troubleshoot these types of errors.

Default startup limits


The ASP.NET Core Module is configured with a default startupTimeLimit of 120 seconds. When left at the
default value, an app may take up to two minutes to start before the module logs a process failure. For
information on configuring the module, see Attributes of the aspNetCore element.

Solución de problemas de errores de inicio de la aplicación


Habilitar el registro de depuración del módulo de ASP.NET Core
Agregue la siguiente configuración de controlador al archivo web.config de la aplicación para habilitar los
registros de depuración del módulo ASP.NET Core:

<aspNetCore ...>
<handlerSettings>
<handlerSetting name="debugLevel" value="file" />
<handlerSetting name="debugFile" value="c:\temp\ancm.log" />
</handlerSettings>
</aspNetCore>

Confirme que la ruta de acceso especificada para el registro exista y que la identidad del grupo de
aplicaciones tenga permisos de escritura en la ubicació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 de trabajo
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 de hospedaje que con la
propia aplicación.
Implementación autocontenida
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 de hospedaje 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.

IMPORTANT
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 para el 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"
hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
</environmentVariables>
</aspNetCore>

<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


Vea Referencia de errores comunes de Azure App Service e IIS con 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.

Obtención de datos de una aplicación


Si una aplicación es capaz de responder a solicitudes, obtenga solicitudes, conexiones y datos adicionales de
la aplicación mediante el middleware en línea del terminal. Para obtener más información y un código de
ejemplo, vea Solución de problemas de proyectos de ASP.NET Core.

Creación de un volcado de memoria


Un volcado de memoria es una instantánea de la memoria del sistema que puede ayudar a determinar la
causa de un bloqueo de la aplicación, un error de inicio o una aplicación lenta.
Bloqueo o excepción de la aplicación
Obtenga y analice un volcado de memoria en Informe de errores de Windows (WER ):
1. Cree una carpeta para almacenar los archivos de volcado de memoria en c:\dumps . El grupo de
aplicaciones debe tener acceso de escritura a la carpeta.
2. Ejecute el script EnableDumps de PowerShell:
Si la aplicación usa el modelo de hospedaje en proceso, ejecute el script w3wp.exe:

.\EnableDumps w3wp.exe c:\dumps

Si la aplicación usa el modelo de hospedaje fuera de proceso, ejecute el script dotnet.exe:

.\EnableDumps dotnet.exe c:\dumps

3. Ejecute la aplicación en las condiciones que hacen que se produzca el bloqueo.


4. Una vez que se haya producido el bloqueo, ejecute el script DisableDumps de PowerShell:
Si la aplicación usa el modelo de hospedaje en proceso, ejecute el script w3wp.exe:

.\DisableDumps w3wp.exe

Si la aplicación usa el modelo de hospedaje fuera de proceso, ejecute el script dotnet.exe:

.\DisableDumps dotnet.exe

Después de que se bloquee la aplicación y se complete la recopilación del volcado de memoria, la aplicación
puede finalizar con normalidad. El script de PowerShell configura WER de modo que recopile un máximo de
cinco volcados de memoria por aplicación.

WARNING
Los volcados de memoria pueden ocupar una gran cantidad de espacio en disco (hasta varios gigabytes cada uno).

La aplicación deja de responder, produce un error durante el inicio o se ejecuta con normalidad
Si una aplicación deja de responder (se detiene, pero no se bloquea), produce un error durante el inicio o se
ejecuta con normalidad, vea User-Mode Dump Files: Choosing the Best Tool (Archivos de volcado de
memoria en modo de usuario: selección de la mejor herramienta) para seleccionar una herramienta
apropiada para generar el volcado de memoria.
Análisis del volcado de memoria
El volcado de memoria se puede analizar de varias maneras. Para obtener más información, vea Analyzing a
User-Mode Dump File (Análisis de un archivo de volcado de memoria en modo de usuario).

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
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
Solución de problemas de proyectos de ASP.NET Core
Controlar errores en ASP.NET Core
Referencia de errores comunes de Azure App Service e IIS con ASP.NET Core
Módulo ASP.NET Core
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
22/05/2019 • 33 minutes to read • Edit Online

Por Luke Latham


En este tema, se ofrece información sobre cómo solucionar errores comunes al hospedar aplicaciones ASP.NET
Core en Azure App Service e IIS.
Recopile la siguiente información:
Comportamiento del explorador (código de estado y mensaje de error)
Entradas de registro de eventos de la aplicación
Azure App Service : vea Solución de problemas de ASP.NET Core en Azure App Service.
IIS
1. Seleccione Inicio en el menú Windows, escriba Visor de eventos y presione Entrar.
2. Una vez abierto el Visor de eventos, amplíe Registros de Windows > Aplicación en la
barra lateral.
Entradas de registro de stdout y depuración de módulo ASP.NET Core
Azure App Service : vea Solución de problemas de ASP.NET Core en Azure App Service.
IIS: siga las instrucciones de las secciones Creación y redireccionamiento de registros y Registros de
diagnóstico mejorados del tema Módulo ASP.NET Core.
Compare la información sobre errores con los siguientes errores comunes. Si se encuentra una coincidencia,
siga los consejos de solución de problemas.
La lista de errores en este tema no es exhaustiva. Si se produce algún error que no aparezca aquí, abra un
problema nuevo mediante el botón Comentarios sobre el contenido situado en la parte inferior de este tema
con instrucciones detalladas sobre cómo reproducir el error.

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: No se ha podido ejecutar
el paquete EXE
†El registro se encuentra en
C:\Users{USER }\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 .NET Core, 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 el
servidor 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 o en Aplicaciones y características. Si se requiere un tiempo
de ejecución específico, descargue el tiempo de ejecución de Archivos de descarga de .NET e instálelo en el
sistema. 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: el archivo DLL del módulo C:\WINDOWS\system32\inetsrv\aspnetcore.dll no se
ha podido cargar. 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. Este problema se produce si ha instalado el módulo ASP.NET Core antes de
una actualización del sistema operativo y, a continuación, ejecuta cualquier grupo de aplicaciones en modo de
32 bits después de una actualización del sistema operativo. 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.

Falta la extensión de sitio, se han instalado extensiones de sitio de 32


bits (x86) y 64 bits (x64) o se ha definido un valor de bits de proceso
incorrecto
Se aplica a las aplicaciones hospedadas por Azure App Services.
Explorador: Error HTTP 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud
en proceso ni se han encontrado dependencias nativas. No se ha podido encontrar el controlador de la
solicitud en proceso. Resultado obtenido al invocar a hostfxr: No se ha podido encontrar ninguna versión
de Framework compatible. El marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-
preview -*" no se ha podido encontrar. No se pudo iniciar la aplicación
"/LM/W3SVC/1416782824/ROOT", código de error "0x8000ffff".
Registro de stdout del módulo de ASP.NET Core: No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-
preview -*" no se ha podido encontrar.
Registro de depuración del módulo de ASP.NET Core: No se ha podido invocar a hostfxr para encontrar
el controlador de la solicitud en proceso ni se han encontrado dependencias nativas. Lo más probable es que
esto signifique que la aplicación no está configurada correctamente. Compruebe las versiones de
Microsoft.NetCore.App y de Microsoft.AspNetCore.App que la aplicación tiene como destino y que están
instaladas en el equipo. Se ha devuelto un valor HRESULT con errores: 0x8000ffff. No se ha podido
encontrar el controlador de la solicitud en proceso. No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado "Microsoft.AspNetCore.App", versión "{VERSION }-preview -*"
no se ha podido encontrar.
Solución del problema:
Si ejecuta la aplicación en un entorno de ejecución en versión preliminar, instale la extensión de sitio de
32 bits (x86) o de 64 bits (x64) que coincida con el valor de bits de la aplicación y la versión del entorno
de ejecución de la aplicación. No instale al mismo tiempo ambas extensiones o varias versiones
del entorno de ejecución de la extensión.
ASP.NET Core {RUNTIME VERSION } (x86) Runtime
ASP.NET Core {RUNTIME VERSION } (x64) Runtime
Reinicie la aplicación. Espere unos segundos a que finalice la operación.
Si ejecuta la aplicación en un entorno de ejecución en versión preliminar y están instaladas las
extensiones de sitio de 32 bits (x86) y 64 bits (x64), desinstale la extensión de sitio que no coincida con el
valor de bits de la aplicación. A continuación, reinicie la aplicación. Espere unos segundos a que finalice la
operación.
Si va a ejecutar la aplicación en un entorno de ejecución en versión preliminar y el valor de bits de la
extensión de sitio coincide con el de la aplicación, confirme que la versión del entorno de ejecución de la
extensión de sitio coincide con la versión del entorno de ejecución de la aplicación.
Confirme que el valor de Plataforma de la aplicación en Configuración de la aplicación coincide con
el valor de bits de la aplicación.
Para obtener más información, vea Implementar aplicaciones de ASP.NET Core en Azure App Service.

Se ha implementado una aplicación x86, pero el grupo de


aplicaciones no está habilitado para aplicaciones de 32 bits
Explorador: Error HTTP 500.30: error de inicio en el proceso ANCM
Registro de aplicación: En la aplicación '/LM/W3SVC/5/ROOT' con la raíz física '{PATH}' se ha
producido una excepción administrada inesperada, código de excepción = '0xe0434352'. Compruebe los
registros stderr para obtener más información. La aplicación '/LM/W3SVC/5/ROOT' con la raíz física
'{PATH}' no ha podido cargar el CLR ni la aplicación administrada. El subproceso de trabajo CLR se ha
cerrado prematuramente
Registro de stdout del módulo ASP.NET Core: El archivo de registro se ha creado, pero está vacío.
Registro de depuración del módulo de ASP.NET Core: Se ha devuelto un valor HRESULT con errores:
0x8007023e
El SDK intercepta este escenario al publicar una aplicación independiente. El SDK genera un error si el RID no
coincide con el destino de plataforma (por ejemplo, RID de win10-x64 con
<PlatformTarget>x86</PlatformTarget> en el archivo del proyecto).

Solución del problema:


En el caso de una implementación dependiente del marco x86 ( <PlatformTarget>x86</PlatformTarget> ), habilite
el grupo de aplicaciones de IIS para aplicaciones de 32 bits. En el Administrador de IIS, abra la Configuración
avanzada del grupo de aplicaciones y establezca Habilitar aplicaciones de 32 bits en True.

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 ha podido iniciar el proceso con la línea de comandos '"C:{{PATH}{ASSEMBLY }.{exe|dll}"',
código de error = '0x80004005 : ff'.
Registro de stdout del módulo ASP.NET Core: Excepción no controlada:
System.BadImageFormatException: No se ha podido cargar el archivo ni el 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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 --O -- No se podido establecer la conexión
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
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: Para usar el módulo ASP.NET Core, se deben instalar las características de
IIS 7.0 CoreWebEngine y W3SVC.
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: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
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 de ASP.NET Core no instalado o permisos


incorrectos
Explorador: 500.19 Error interno del servidor: no se puede acceder a la página solicitada porque los
datos de configuración relacionados de la página no son válidos. --O -- No se puede mostrar esta página
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
Confirme que está habilitado el rol adecuado. Vea Configuración de IIS.
Abra Programas y características o Aplicaciones y características y confirme que Hospedaje de
Windows Server está instalado. Si Hospedaje de Windows Server no está presente en la lista de
programas instalados, descargue e instale el conjunto de hospedaje de .NET Core.
Instalador del conjunto de hospedaje de .NET Core actual (descarga directa)
Para obtener más información, consulte Instalar el conjunto 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.
Si ha desinstalado el paquete de hospedaje de ASP.NET Core y ha instalado una versión anterior de
dicho paquete, el archivo applicationHost.config no incluirá ninguna sección para el módulo ASP.NET
Core. Abra applicationHost.config en %windir%/System32/inetsrv/config y busque el grupo de secciones
<configuration><configSections><sectionGroup name="system.webServer"> . Si falta la sección del módulo
ASP.NET Core en el grupo de secciones, añada el elemento de sección:

<section name="aspNetCore" overrideModeDefault="Allow" />

Si quiere, también puede instalar la versión más reciente del paquete de hospedaje de ASP.NET Core. La
versión más reciente es compatible con las versiones anteriores de las aplicaciones ASP.NET Core
admitidas.

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 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: La aplicación 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY }' con la raíz
física 'C:{{PATH}'' no ha podido iniciar el proceso con la línea de comandos '"{...}" ', código de error = '0 x
80070002 : 0. La aplicación '{PATH}' no se ha podido iniciar. No se ha encontrado el archivo ejecutable en
'{PATH}'. No se ha podido iniciar la aplicación '/LM/W3SVC/2/ROOT', código de error '0x8007023e'.
Registro de stdout del módulo ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: Registro de eventos: "La aplicación '{PATH}'"
no se ha podido iniciar. No se ha encontrado el archivo ejecutable en '{PATH}'. Se ha devuelto un valor
HRESULT con errores: 0x8007023e
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 ha podido iniciar el proceso con la línea de comandos '"{...}" ', código de error = '0 x
80070002 : 0.
Registro de stdout del módulo ASP.NET Core: El archivo de registro se ha creado, pero está 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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, es posible que la identidad del usuario del grupo de aplicaciones no pueda
acceder a dotnet.exe. Confirme que la identidad del usuario del grupo de aplicaciones tiene acceso al
directorio C:\Archivos de programa\dotnet. Confirme que no haya ninguna regla de denegación
configurada para la identidad del usuario del grupo de aplicaciones 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.
Instalador del conjunto de hospedaje de .NET Core actual (descarga directa)
Para obtener más información, consulte Instalar el conjunto de hospedaje de .NET Core.
Si se requiere un tiempo de ejecución específico, descargue el tiempo de ejecución de Archivos de
descarga de .NET e instálelo en el sistema. 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 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud
en proceso ni se han encontrado dependencias nativas. Lo más probable es que esto signifique que la
aplicación no está configurada correctamente. Compruebe las versiones de Microsoft.NetCore.App y de
Microsoft.AspNetCore.App que la aplicación tiene como destino y que están instaladas en el equipo. No
se ha podido encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a hostfxr:
¿pretendía ejecutar comandos SDK de dotnet? Instale el SDK de dotnet de:
https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409 No se ha podido iniciar la aplicación
'/LM/W3SVC/3/ROOT', código de error '0x8000ffff'.
Registro de stdout del módulo ASP.NET Core: ¿pretendía ejecutar comandos SDK de dotnet? Instale
el SDK de dotnet de: https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
Registro de depuración del módulo de ASP.NET Core: No se ha podido invocar a hostfxr para
encontrar el controlador de la solicitud en proceso ni se han encontrado dependencias nativas. Lo más
probable es que esto signifique que la aplicación no está configurada correctamente. Compruebe las
versiones de Microsoft.NetCore.App y de Microsoft.AspNetCore.App que la aplicación tiene como
destino y que están instaladas en el equipo. Se ha devuelto un valor HRESULT con errores: 0x8000ffff
No se ha podido encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a
hostfxr: ¿pretendía ejecutar comandos SDK de dotnet? Instale el SDK de dotnet de:
https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409 Se ha devuelto un valor HRESULT con
errores: 0x8000ffff
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 ha podido iniciar el proceso con la línea de comandos '"dotnet" .
{ASSEMBLY }.dll', código de error = '0x80004005 : 80008081.
Registro de stdout del módulo de 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 obtener más información, consulte Solución de problemas (IIS ) o
Solución de problemas (Azure App Service).
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 es una lista de argumentos de la aplicación (
arguments="{ARGUMENT_1}, {ARGUMENT_2}, ... {ARGUMENT_X}" ) para una implementación independiente
(SCD ).

Falta el marco compartido de .NET Core


Explorador: Error HTTP 500.0: error de carga del controlador en proceso ANCM
Registro de aplicación: No se ha podido invocar a hostfxr para encontrar el controlador de la solicitud
en proceso ni se han encontrado dependencias nativas. Lo más probable es que esto signifique que la
aplicación no está configurada correctamente. Compruebe las versiones de Microsoft.NetCore.App y de
Microsoft.AspNetCore.App que la aplicación tiene como destino y que están instaladas en el equipo. No
se ha podido encontrar el controlador de la solicitud en proceso. Resultado obtenido al invocar a hostfxr:
No se ha podido encontrar ninguna versión de Framework compatible. El marco especificado
'Microsoft.AspNetCore.App', versión '{VERSION }', no se ha podido encontrar.
No se ha podido iniciar la aplicación '/LM/W3SVC/5/ROOT', código de error '0x8000ffff'.
Registro de stdout del módulo de ASP.NET Core: No se ha podido encontrar ninguna versión de
Framework compatible. El marco especificado 'Microsoft.AspNetCore.App', versión '{VERSION }', no se
ha podido encontrar.
Registro de depuración del módulo de ASP.NET Core: Se ha devuelto un valor HRESULT con
errores: 0x8000ffff
Solución del problema:
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.

Grupo de aplicaciones detenido


Explorador: 503 Servicio no disponible
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
Confirme que el grupo de aplicaciones no está en estado Detenido.

La aplicación secundaria incluye una sección de <controladores>


Explorador: error HTTP 500.19: error interno del servidor
Registro de aplicación: No hay ninguna entrada
Registro de stdout del módulo de ASP.NET Core: El archivo de registro de la aplicación raíz se ha
creado y muestra un funcionamiento normal. No se ha creado el archivo de registro de la aplicación
secundaria.
Registro de depuración del módulo de ASP.NET Core: El archivo de registro de la aplicación raíz se ha
creado y muestra un funcionamiento normal. No se ha creado el archivo de registro de la aplicación
secundaria.
Solución del problema:
Confirme que el archivo web.config de la aplicación secundaria no incluye una sección <handlers> o que la
aplicación secundaria no hereda los controladores de la aplicación primaria.
La sección <system.webServer>de la aplicación primaria de web.config está colocada dentro de un elemento
<location> . La propiedad InheritInChildApplications está establecida en false para indicar que las
aplicaciones que residen en un subdirectorio de la aplicación primaria no heredan la configuración especificada
en el elemento <location>. Para obtener más información, vea Módulo ASP.NET Core.
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: No se ha podido iniciar la redirección de stdout en C:\Archivos de
programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Mensaje de excepción: HRESULT 0 x
80070005 se ha devuelto en {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. No se
ha podido detener la redirección de stdout en C:\Archivos de programa\IIS\Asp.Net Core
Module\V2\aspnetcorev2.dll. Mensaje de excepción: HRESULT 0 x 80070002 se ha devuelto en {PATH}.
No se ha podido iniciar la redirección de stdout en {PATH}\aspnetcorev2_inprocess.dll.
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Registro de depuración del módulo de ASP.NET Core: No se ha podido iniciar la redirección de
stdout en C:\Archivos de programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Mensaje de
excepción: HRESULT 0 x 80070005 se ha devuelto en
{PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. No se ha podido detener la
redirección de stdout en C:\Archivos de programa\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll.
Mensaje de excepción: HRESULT 0 x 80070002 se ha devuelto en {PATH}. No se ha podido iniciar la
redirección de stdout en {PATH}\aspnetcorev2_inprocess.dll.
Registro de aplicación: Advertencia: No se ha podido crear el archivo de registro de stdout \?
{PATH}\path_doesnt_exist\stdout_{PROCESS ID }{TIMESTAMP }.log, código de error = -2147024893.
Registro de stdout del módulo de ASP.NET Core: No se ha creado el archivo de registro.
Solución del problema:
La ruta de acceso stdoutLogFile especificada en el elemento <aspNetCore> de web.config no existe. Para
obtener más información, consulte ASP.NET Core Module: Log creation and redirection (Creación y
redireccionamiento de registros: módulo de ASP.NET Core).
El usuario del grupo de aplicaciones no tiene acceso de escritura a la ruta de acceso del registro de
stdout.

Problema general de configuración de aplicación


Explorador: error HTTP 500.0: error de carga del controlador en proceso ANCM --O -- Error HTTP
500.30: error de inicio en el proceso ANCM
Registro de aplicación: Variable
Registro de stdout del módulo de ASP.NET Core: El archivo de registro se ha creado, pero está vacío
o se ha creado con entradas normales, hasta el punto en que se producen errores en la aplicación.
Registro de depuración del módulo de ASP.NET Core: Variable
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}'' ha creado 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 "{PORT}", código de error =
'{ERROR CODE }'
Registro de stdout del módulo de ASP.NET Core: El archivo de registro se ha creado, pero está vacío.
Solución del problema:
El proceso no se ha iniciado, probablemente debido a un problema de configuración o programación de la
aplicación.
Para obtener más información, vea los temas siguientes:
Solución de problemas de ASP.NET Core en IIS
Solución de problemas de ASP.NET Core en Azure App Service
Solución de problemas de proyectos de ASP.NET Core
Transformación de web.config
17/05/2019 • 4 minutes to read • Edit Online

Por Vijay Ramakrishnan y Luke Latham


Las transformaciones en el archivo web.config se pueden aplicar automáticamente al publicarse una aplicación en
función de:
Configuración de compilación
Profile
Entorno
Custom
Estas transformaciones se producen en cualquiera de los escenarios de generación web.config siguientes:
Generadas automáticamente por el SDK Microsoft.NET.Sdk.Web .
Proporcionadas por el desarrollador en la raíz del contenido de la aplicación.

Configuración de compilación
Las transformaciones de la configuración de compilación se ejecutan en primer lugar.
Incluya un archivo web.{CONFIGURATION }.config para cada configuración de compilación (Debug|Release) que
requiera una transformación de web.config.
En el siguiente ejemplo, una variable de entorno específica de la configuración se establece en web.Release.config:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<location>
<system.webServer>
<aspNetCore>
<environmentVariables xdt:Transform="InsertIfMissing">
<environmentVariable name="Configuration_Specific"
value="Configuration_Specific_Value"
xdt:Locator="Match(name)"
xdt:Transform="InsertIfMissing" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>

La transformación se aplica cuando la configuración se establece en Release:

dotnet publish --configuration Release

La propiedad MSBuild de la configuración es $(Configuration) .

Perfil
Las transformaciones del perfil se ejecutan en segundo lugar, una vez transformada la configuración de
compilación.
Incluya un archivo web.{PROFILE }.config para cada configuración de perfil que requiera una transformación de
web.config.
En el siguiente ejemplo, una variable de entorno específica del perfil se establece en web.FolderProfile.config para
un perfil de publicación de carpetas:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<location>
<system.webServer>
<aspNetCore>
<environmentVariables xdt:Transform="InsertIfMissing">
<environmentVariable name="Profile_Specific"
value="Profile_Specific_Value"
xdt:Locator="Match(name)"
xdt:Transform="InsertIfMissing" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>

La transformación se aplica cuando el perfil es FolderProfile:

dotnet publish --configuration Release /p:PublishProfile=FolderProfile

La propiedad MSBuild del nombre de perfil es $(PublishProfile) .


Si no se pasa ningún perfil, el nombre de perfil predeterminado es FileSystem y web.FileSystem.config se aplica
si el archivo está presente en la raíz del contenido de la aplicación.

Entorno
Las transformaciones del entorno se ejecutan en tercer lugar, una vez transformados la configuración de
compilación y el perfil.
Incluya un archivo web.{ENVIRONMENT }.config para cada entorno que requiera una transformación de
web.config.
En el siguiente ejemplo, una variable de entorno específica del entorno se establece en web.Production.config
para el entorno de producción:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<location>
<system.webServer>
<aspNetCore>
<environmentVariables xdt:Transform="InsertIfMissing">
<environmentVariable name="Environment_Specific"
value="Environment_Specific_Value"
xdt:Locator="Match(name)"
xdt:Transform="InsertIfMissing" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>

La transformación se aplica cuando el entorno es Production:


dotnet publish --configuration Release /p:EnvironmentName=Production

La propiedad MSBuild del entorno es $(EnvironmentName) .


Al publicar desde Visual Studio y usar un perfil de publicación, consulte Perfiles de publicación de Visual Studio
para la implementación de aplicaciones ASP.NET Core.
La variable de entorno ASPNETCORE_ENVIRONMENT se agrega automáticamente al archivo web.config al especificarse
el nombre del entorno.

Personalizados
Las transformaciones personalizadas se ejecutan en último lugar, una vez transformados la configuración de
compilación, el perfil y el entorno.
Incluya un archivo {CUSTOM_NAME }.transform para cada configuración personalizada que requiera una
transformación de web.config.
En el siguiente ejemplo, una variable de entorno de transformación personalizada se establece en
custom.transform:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<location>
<system.webServer>
<aspNetCore>
<environmentVariables xdt:Transform="InsertIfMissing">
<environmentVariable name="Custom_Specific"
value="Custom_Specific_Value"
xdt:Locator="Match(name)"
xdt:Transform="InsertIfMissing" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>

La transformación se aplica cuando la propiedad CustomTransformFileName se pasa al comando dotnet publish:

dotnet publish --configuration Release /p:CustomTransformFileName=custom.transform

La propiedad MSBuild del nombre de perfil es $(CustomTransformFileName) .

Evitar la transformación de web.config


Para evitar las transformaciones del archivo web.config, establezca la propiedad MSBuild
$(IsWebConfigTransformDisabled) :

dotnet publish /p:IsWebConfigTransformDisabled=true

Recursos adicionales
Sintaxis de transformación de Web.config para la implementación de proyectos de aplicación web
Sintaxis de transformación de Web.config para la implementación de proyectos web usando Visual Studio
Implementación del servidor web Kestrel en
ASP.NET Core
02/07/2019 • 46 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 los siguientes escenarios:
HTTPS
Actualización opaca para habilitar WebSockets
Sockets de Unix para alto rendimiento detrás de Nginx
HTTP/2 (excepto en macOS†)
†HTTP/2 se admitirá en una versión futura en macOS.
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)

Compatibilidad con HTTP/2


HTTP/2 está disponible para las aplicaciones de ASP.NET Core si se cumplen los siguientes
requisitos básicos:
Sistema operativo†
Windows Server 2016/Windows 10 o posterior‡
Linux con OpenSSL 1.0.2 o posterior (por ejemplo, Ubuntu 16.04 o posterior)
Plataforma de destino: .NET Core 2.2 o posterior
Conexión con Application-Layer Protocol Negotiation (ALPN )
Conexión con TLS 1.2 o una versión posterior
†HTTP/2 se admitirá en una versión futura en macOS. ‡Kestrel tiene compatibilidad limitada para
HTTP/2 en Windows Server 2012 R2 y Windows 8.1. La compatibilidad es limitada porque la
lista de conjuntos de cifrado TLS admitidos y disponibles en estos sistemas operativos está
limitada. Se puede requerir un certificado generado mediante Elliptic Curve Digital Signature
Algorithm (ECDSA) para proteger las conexiones TLS.
Si se establece una conexión HTTP/2, HttpRequest.Protocol notifica HTTP/2 .
HTTP/2 está deshabilitado de manera predeterminada. Para obtener más información sobre la
configuración, consulte las secciones Opciones de Kestrel y ListenOptions.Protocols.

Cuándo usar Kestrel con un proxy inverso


Kestrel se puede usar por sí solo o con un servidor proxy inverso, como Internet Information
Services (IIS ), Nginx o Apache. Un servidor proxy inverso recibe las solicitudes HTTP de la red y
las reenvía a Kestrel.
Kestrel empleado como servidor web perimetral (accesible desde Internet):

Kestrel empleado en una configuración de proxy inverso:

Cualquiera de las configuraciones, —con o sin un servidor proxy inverso—, es una configuración
de hospedaje admitida para ASP.NET 2.1 o aplicaciones posteriores que reciben solicitudes de
Internet.
Kestrel, utilizado como un servidor perimetral sin un servidor proxy inverso, 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 de los
encabezados Host de las solicitudes. 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.
Un proxy inverso puede hacer lo siguiente:
Limitar el área expuesta públicamente de las aplicaciones que hospeda.
Proporcionar una capa extra de configuración y defensa.
Posiblemente, integrarse mejor con la infraestructura existente.
Simplificar el equilibrio de carga y la configuración de una comunicación segura (HTTPS ).
Solo el servidor proxy inverso requiere un certificado X.509, y dicho servidor se puede
comunicar con los servidores de aplicaciones en la red interna por medio de HTTP sin
formato.

WARNING
El hospedaje en una configuración de proxy inverso requiere filtrado de hosts.

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 :

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
// Set properties and call methods on options
});

Si la aplicación no llama a CreateDefaultBuilder para configurar el host, llame a UseKestrel


antes de llamar a ConfigureKestrel :

public static void Main(string[] args)


{
var host = new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
// Set properties and call methods on options
})
.Build();

host.Run();
}

Para proporcionar configuración adicional después de llamar a CreateDefaultBuilder , llame a


UseKestrel:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
// Set properties and call methods on options
});

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.
Establezca restricciones en la propiedad Limits de la clase KestrelServerOptions. La propiedad
Limits contiene una instancia de la clase KestrelServerLimits.
En los ejemplos siguientes se usa el espacio de nombres
Microsoft.AspNetCore.Server.Kestrel.Core:

using Microsoft.AspNetCore.Server.Kestrel.Core;

Tiempo de expiración de la conexión persistente


KeepAliveTimeout
Obtiene o establece el tiempo de expiración de la conexión persistente. El valor predeterminado
es de 2 minutos.

.ConfigureKestrel((context, 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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
});

Las conexiones máximas de cliente


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;
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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
});

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.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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.MaxConcurrentUpgradedConnections = 100;
});

El número máximo de conexiones es ilimitado de forma predeterminada (null).


El tamaño máximo del cuerpo de 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 RequestSizeLimitAttribute 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.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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024;
});

Puede invalidar la configuración en una solicitud específica de middleware:


app.Run(async (context) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;

var minRequestRateFeature =
context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
var minResponseRateFeature =
context.Features.Get<IHttpMinResponseDataRateFeature>();

if (minRequestRateFeature != null)
{
minRequestRateFeature.MinDataRate = new MinDataRate(
bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
}

if (minResponseRateFeature != null)
{
minResponseRateFeature.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.
Cuando se ejecuta una aplicación fuera de proceso detrás del módulo de ASP.NET Core, el límite
de tamaño del cuerpo de la solicitud de Kestrel se deshabilita porque IIS ya establece el límite.
La velocidad mínima de los datos del cuerpo de 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.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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
});

Puede invalidar los límites de velocidad mínima por solicitud en el middleware:

app.Run(async (context) =>


{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;

var minRequestRateFeature =
context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
var minResponseRateFeature =
context.Features.Get<IHttpMinResponseDataRateFeature>();

if (minRequestRateFeature != null)
{
minRequestRateFeature.MinDataRate = new MinDataRate(
bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
}

if (minResponseRateFeature != null)
{
minResponseRateFeature.MinDataRate = new MinDataRate(
bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
}

La característica IHttpMinResponseDataRateFeature a la que se hace referencia en el ejemplo


anterior no está presente en HttpContext.Features para las solicitudes HTTP/2, porque
generalmente no se permite modificar los límites de velocidad por solicitud debido a la
compatibilidad del protocolo con la multiplexación de solicitudes. Sin embargo,
IHttpMinRequestBodyDataRateFeature sigue estando presente en HttpContext.Features para las
solicitudes HTTP/2, ya que el límite de velocidad de lectura aún puede estar completamente
deshabilitado por solicitud estableciendo IHttpMinRequestBodyDataRateFeature.MinDataRate en
null incluso para una solicitud HTTP/2. Si se intenta leer
IHttpMinRequestBodyDataRateFeature.MinDataRate o se intenta su establecimiento en un valor
distinto de null , se obtendrá una excepción NotSupportedException con una solicitud HTTP/2
dada.
Se siguen aplicando límites de velocidad en todo el servidor configurados con
KestrelServerOptions.Limits a las conexiones HTTP/1.x y HTTP/2.

Ninguna de las características de velocidad a las que se hace referencia en el ejemplo anterior
están presentes en HttpContext.Features para las solicitudes HTTP/2, porque no se permite
modificar los límites de velocidad por solicitud en HTTP/2 debido a la compatibilidad del
protocolo con la multiplexación de solicitudes. Se siguen aplicando límites de velocidad en todo el
servidor configurados con KestrelServerOptions.Limits a las conexiones HTTP/1.x y HTTP/2.
Tiempo de expiración de los encabezados de solicitud
RequestHeadersTimeout
Obtiene o establece la cantidad máxima de tiempo que el servidor pasa recibiendo las cabeceras
de las solicitudes. El valor predeterminado es 30 segundos.

.ConfigureKestrel((context, 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");
});
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});

Secuencias máximas por conexión


Http2.MaxStreamsPerConnection limita el número de secuencias de solicitudes simultáneas por
conexión HTTP/2. Se rechazarán las secuencias en exceso.

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.MaxStreamsPerConnection = 100;
});
El valor predeterminado es 100.
Tamaño de la tabla de encabezado
El descodificador HPACK descomprime los encabezados HTTP para las conexiones HTTP/2.
Http2.HeaderTableSize limita el tamaño de la tabla de compresión de encabezado que usa el
descodificador HPACK. El valor se proporciona en octetos y debe ser mayor que cero (0).

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.HeaderTableSize = 4096;
});

El valor predeterminado es 4096.


Tamaño máximo de marco
Http2.MaxFrameSize indica el tamaño máximo de la carga útil de la trama de conexión HTTP/2
que se puede recibir. El valor se proporciona en octetos y debe estar comprendido entre 2^14 (16
384) y 2^24-1 (16 777 215).

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.MaxFrameSize = 16384;
});

El valor predeterminado es 2^14 (16 384).


Tamaño máximo del encabezado de solicitud
Http2.MaxRequestHeaderFieldSize indica el tamaño máximo permitido (en octetos) de los valores
de los encabezados de solicitud. Este límite se aplica al nombre y al valor conjuntamente en sus
representaciones comprimidas y no comprimidas. El valor debe ser mayor que cero (0).

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
});

El valor predeterminado es 8192.


Tamaño inicial de la ventana de conexión
Http2.InitialConnectionWindowSize indica la cantidad máxima de datos del cuerpo de solicitud (en
bytes) que el servidor almacena en búfer a la vez de forma agregada en todas las solicitudes
(transmisiones) por conexión. Las solicitudes también están limitadas por
Http2.InitialStreamWindowSize . El valor debe ser igual o mayor que 65 535 y menor que 2^31
(2 147 483 648).
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.InitialConnectionWindowSize = 131072;
});

El valor predeterminado es 128 KB (131 072).


Tamaño inicial de la ventana de transmisión
Http2.InitialStreamWindowSize indica la cantidad máxima de datos del cuerpo de solicitud (en
bytes) que el servidor almacena en búfer a la vez por solicitud (transmisión). Las solicitudes
también están limitadas por Http2.InitialStreamWindowSize . El valor debe ser igual o mayor que
65 535 y menor que 2^31 (2 147 483 648).

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Limits.Http2.InitialStreamWindowSize = 98304;
});

El valor predeterminado es 96 KB (98 304).


E/S sincrónica
AllowSynchronousIO controla si se permite la E/S sincrónica para la solicitud y la respuesta. El
valor predeterminado es false .
AllowSynchronousIO controla si se permite la E/S sincrónica para la solicitud y la respuesta. El
valor predeterminado es true .

WARNING
Un gran número de operaciones de E/S sincrónicas de bloqueo puede dar lugar al colapso del grupo de
subprocesos, lo que hace que la aplicación no responda. Habilite solo AllowSynchronousIO al usar una
biblioteca que no admite la E/S asincrónica.

En el siguiente ejemplo se habilita la E/S sincrónica:

.ConfigureKestrel((context, options) =>


{
options.AllowSynchronousIO = true;
});

En el siguiente ejemplo se deshabilita la E/S sincrónica:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(options =>
{
options.AllowSynchronousIO = false;
});
Para más información sobre otras opciones y límites de Kestrel, vea:
KestrelServerOptions
KestrelServerLimits
ListenOptions

Configuración de punto de conexión


ASP.NET Core enlaza de forma predeterminada a:
http://localhost:5000
https://localhost:5001 (cuando hay presente un certificado de desarrollo local)

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 .

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"
).
Para más información sobre estos enfoques, consulte Direcciones URL del servidor e Invalidar la
configuración.
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 de KestrelServerOptions para configurar los
puertos y los prefijos de dirección URL para 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 ).
ASP.NET Core 2.1 o configuración KestrelServerOptions posterior:
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.
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureEndpointDefaults(configureOptions =>
{
configureOptions.NoDelay = true;
});
});
webBuilder.UseStartup<Startup>();
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.ConfigureEndpointDefaults(configureOptions =>
{
configureOptions.NoDelay = true;
});
});

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.

Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureHttpsDefaults(options =>
{
// certificate is an X509Certificate2
options.ServerCertificate = certificate;
});
});
webBuilder.UseStartup<Startup>();
});

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
// certificate is an X509Certificate2
httpsOptions.ServerCertificate = certificate;
});
});

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).
Reemplazar el certificado predeterminado de configuración
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; required>",
"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; required>",
"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.
options.Configure(context.Configuration.GetSection("Kestrel")) devuelve un
con un método .Endpoint(string name, options => { }) que
KestrelConfigurationLoader
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 tener acceso directamente a KestrelServerOptions.ConfigurationLoader


para proseguir con la iteración en el cargador existente, como la proporcionada por
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.
KestrelConfigurationLoaderrefleja 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 ConfigureHttpsDefaults se 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.
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.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;
};
});
});
});
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.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;
};
});
});
})
.Build();

Enlazar a un socket TCP


El método Listen se enlaza a un socket TCP y una expresión lambda de opciones permite
configurar un certificado X.509:

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, 5000);
options.Listen(IPAddress.Loopback, 5001, 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, 5000);
options.Listen(IPAddress.Loopback, 5001, 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, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});

En el ejemplo se configura HTTPS 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
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");
});
});
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.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

Limitaciones
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. Sin
embargo, tenga en cuenta las siguientes limitaciones:
HTTPS 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.
ListenOptions.Protocols
La propiedad Protocols establece los protocolos HTTP ( HttpProtocols ) habilitados en un punto
de conexión o para el servidor. Asigne un valor a la propiedad Protocols desde el valor de
enumeración HttpProtocols .

VALOR DE ENUMERACIÓN HTTPPROTOCOLS PROTOCOLO DE CONEXIÓN PERMITIDO

Http1 HTTP/1.1 solo. Puede usarse con o sin TLS.

Http2 HTTP/2 solo. Se usa principalmente con TLS. Se


pueden utilizar sin TLS solo si el cliente admite un
modo de conocimientos previos.

Http1AndHttp2 HTTP/1.1 y HTTP/2. Requiere una conexión TLS y


Application-Layer Protocol Negotiation (ALPN) para
negociar HTTP/2; de lo contrario, la opción
predeterminada para la conexión es HTTP/1.1.

El protocolo predeterminado es HTTP/1.1.


Restricciones de TLS para HTTP/2:
TLS 1.2 o versiones posteriores
Renegociación deshabilitada
Compresión deshabilitada
Tamaños de intercambio de claves efímeras mínimos:
Curva elíptica Diffie-Hellman (ECDHE ) [RFC4492]; 224 bits como mínimo
Campo finito Diffie-Hellman (DHE ) [ TLS12 ]; 2048 bits como mínimo
Conjunto de cifrado no restringido
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 [ TLS-ECDHE ] con la curva elíptica P -256 [ FIPS186 ] es
compatible de forma predeterminada.
El siguiente ejemplo permite conexiones HTTP/1.1 y HTTP/2 en el puerto 8000. Las conexiones
se protegen mediante TLS con un certificado proporcionado:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Any, 8000, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});

Opcionalmente, cree una implementación IConnectionAdapter para filtrar los protocolos de


enlace TLS en una base por conexión para los cifrados específicos:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Any, 8000, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps("testCert.pfx", "testPassword");
listenOptions.ConnectionAdapters.Add(new TlsFilterAdapter());
});
});
private class TlsFilterAdapter : IConnectionAdapter
{
public bool IsHttps => false;

public Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)


{
var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();

// Throw NotSupportedException for any cipher algorithm that you don't


// wish to support. Alternatively, define and compare
// ITlsHandshakeFeature.CipherAlgorithm to a list of acceptable cipher
// suites.
//
// A ITlsHandshakeFeature.CipherAlgorithm of CipherAlgorithmType.Null
// indicates that no cipher algorithm supported by Kestrel matches the
// requested algorithm(s).
if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
{
throw new NotSupportedException("Prohibited cipher: " +
tlsFeature.CipherAlgorithm);
}

return Task.FromResult<IAdaptedConnection>(new
AdaptedConnection(context.ConnectionStream));
}

private class AdaptedConnection : IAdaptedConnection


{
public AdaptedConnection(Stream adaptedStream)
{
ConnectionStream = adaptedStream;
}

public Stream ConnectionStream { get; }

public void Dispose()


{
}
}
}

Defina el protocolo a partir de la configuración


CreateDefaultBuilder llama a
serverOptions.Configure(context.Configuration.GetSection("Kestrel")) de forma predeterminada
para cargar la configuración de Kestrel.
En el siguiente ejemplo de appsettings.json, se establece un protocolo de conexión
predeterminado (HTTP/1.1 y HTTP/2) para todos los puntos de conexión de Kestrel:

{
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http1AndHttp2"
}
}
}

El siguiente ejemplo de archivo de configuración establece un protocolo de conexión para un


punto de conexión específico:
{
"Kestrel": {
"Endpoints": {
"HttpsDefaultCert": {
"Url": "https://localhost:5001",
"Protocols": "Http1AndHttp2"
}
}
}
}

Los protocolos especificados en el código invalidan los valores establecidos por la configuración.

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 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="<LATEST_VERSION>" />

Llame a 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 HTTPS al configurar
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
El hospedaje en una configuración de proxy inverso requiere filtrado de hosts.

Nombre localhost del host con el número de puerto o la IP de bucle invertido con el
número de puerto

http://localhost:5000/
http://127.0.0.1:5000/
http://[::1]:5000/

Cuando se especifica localhost , Kestrel intenta enlazar a las interfaces de bucle invertido
de IPv4 e IPv6. Si el puerto solicitado lo está usando otro servicio en cualquier interfaz de
bucle invertido, Kestrel no se puede iniciar. Si ninguna de estas interfaces de bucle
invertido está disponible por cualquier otra razón (normalmente porque no se admite
IPv6), Kestrel registra una advertencia.

Filtrado de hosts
Si bien Kestrel admite una configuración basada en prefijos como http://example.com:5000 , pasa
por alto completamente el nombre de host. El host localhost es un caso especial que se usa
para enlazar a direcciones de bucle invertido. Cualquier otro host que no sea una dirección IP
explícita se enlaza a todas las direcciones IP públicas. Los encabezados Host no están validados.
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). El 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
Middleware de encabezados reenviados también tiene una opción 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 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 de acceso público o cuando el
encabezado Host se reenvía directamente.
Para obtener más información sobre el Middleware de encabezados reenviados, consulte Configuración
de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Recursos adicionales
Solución de problemas de proyectos de ASP.NET Core
Exigir HTTPS en ASP.NET Core
Configuración de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
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])
Implementación del servidor web HTTP.sys en
ASP.NET Core
05/07/2019 • 22 minutes to read • Edit Online

Por Tom Dykstra, Chris Ross y Stephen Halter


HTTP.sys es un servidor web de ASP.NET Core que solo se ejecuta en Windows. HTTP.sys supone una
alternativa al servidor 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.
Compatibilidad con HTTP/2
HTTP/2 está habilitado para las aplicaciones de ASP.NET Core si se cumplen los siguientes requisitos
básicos:
Windows Server 2016/Windows 10 o posterior
Conexión con Application-Layer Protocol Negotiation (ALPN )
Conexión con TLS 1.2 o una versión posterior
Si se establece una conexión HTTP/2, HttpRequest.Protocol notifica HTTP/2 .
Si se establece una conexión HTTP/2, HttpRequest.Protocol notifica HTTP/1.1 .
HTTP/2 está habilitado de forma predeterminada. Si no se establece una conexión HTTP/2, la conexión
vuelve a HTTP/1.1. En una futura versión de Windows, las marcas de configuración de HTTP/2 estarán
disponibles, incluida la capacidad para deshabilitar HTTP/2 con 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 al compilar el host y especifique las opciones
HttpSysOptions necesarias. En el siguiente ejemplo se establecen las opciones en sus valores
predeterminados:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.AllowSynchronousIO = false;
options.Authentication.Schemes = AuthenticationSchemes.None;
options.Authentication.AllowAnonymous = true;
options.MaxConnections = null;
options.MaxRequestBodySize = 30000000;
options.UrlPrefixes.Add("http://localhost:5000");
});
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.AllowSynchronousIO = true;
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 false


entrada/salida sincrónica de los
objetos
HttpContext.Request.Body y
HttpContext.Response.Body .

Authentication.AllowAnonymous Permitir solicitudes anónimas. true

Authentication.Schemes Especificar los esquemas de None


autenticación permitidos. Puede
modificarse en cualquier momento
antes de eliminar el agente de
escucha. Los valores se
proporcionan con la enumeración
AuthenticationSchemes: Basic ,
Kerberos , Negotiate , None y
NTLM .

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.
PROPIEDAD. DESCRIPCIÓN DEFAULT

MaxRequestBodySize Vea la sección MaxRequestBodySize. 30 000 000 bytes


(~28,6 MB)

RequestQueueLimit Número máximo de solicitudes que 1000


se pueden poner en cola.

ThrowWriteExceptions Indicar si las escrituras del cuerpo de false


respuesta que no se producen (finalizar con normalidad)
debido a desconexiones del cliente
deben iniciar excepciones o finalizar
con normalidad.

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.DrainEntity
Body – Tiempo permitido
para que la API HTTP Server
purgue el cuerpo de la
entidad en una conexión
persistente.
TimeoutManager.EntityBody
– Tiempo permitido para que
llegue el cuerpo de la entidad
de solicitud.
TimeoutManager.HeaderWait
– Tiempo permitido para que
la API HTTP Server analice el
encabezado de solicitud.
TimeoutManager.IdleConnec
tion – Tiempo permitido para
una conexión inactiva.
TimeoutManager.MinSendBy
tesPerSecond – Tasa mínima
de envío de la respuesta.
TimeoutManager.RequestQu
eue – Tiempo permitido para
que la solicitud permanezca
en la cola de solicitudes antes
de que la aplicación la recoja.

UrlPrefixes Especifique UrlPrefixCollection para


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

PROPIEDAD. DESCRIPCIÓN DEFAULT


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

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 MaxRequestBodySize. 30 000 000 bytes


(~28,6 MB)

RequestQueueLimit Número máximo de solicitudes que 1000


se pueden poner en cola.

ThrowWriteExceptions Indicar si las escrituras del cuerpo de false


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.DrainEntity
Body – Tiempo permitido
para que la API HTTP Server
purgue el cuerpo de la
entidad en una conexión
persistente.
TimeoutManager.EntityBody
– Tiempo permitido para que
llegue el cuerpo de la entidad
de solicitud.
TimeoutManager.HeaderWait
– Tiempo permitido para que
la API HTTP Server analice el
encabezado de solicitud.
TimeoutManager.IdleConnec
tion – Tiempo permitido para
una conexión inactiva.
TimeoutManager.MinSendBy
tesPerSecond – Tasa mínima
de envío de la respuesta.
TimeoutManager.RequestQu
eue – Tiempo permitido para
que la solicitud permanezca
en la cola de solicitudes antes
de que la aplicación la recoja.

UrlPrefixes Especifique UrlPrefixCollection para


registrarse 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
IHttpMaxRequestBodySizeFeature:

public void Configure(IApplicationBuilder app, IHostingEnvironment env,


ILogger<Startup> logger, IServer server)
{
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();
}
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 es para 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. Determine los puertos que se van a abrir para la aplicación y use el Firewall de Windows o el
cmdlet New -NetFirewallRule de PowerShell para abrir los puertos del firewall y permitir que el
tráfico llegue a HTTP.sys. En los comandos y la configuración de la aplicación siguientes, se usa el
puerto 443.
2. Cuando realice una implementación en una máquina virtual de Azure, abra los puertos del grupo
de seguridad de red. En los comandos y la configuración de la aplicación siguientes, se usa el
puerto 443.
3. Si es necesario, obtenga e instale los certificados X.509.
En Windows, puede crear certificados autofirmados con el cmdlet New -SelfSignedCertificate de
PowerShell. Para obtener un ejemplo no compatible, vea UpdateIISExpressSSLForChrome.ps1.
Instale certificados autofirmados o firmados por CA en el almacén personal del equipo > local
del servidor.
4. 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 del entorno de
ejecución de .NET Core desde las descargas de .NET. No instale el SDK completo en el
servidor.
.NET Framework: si la aplicación requiere .NET Framework, consulte la guía de instalación de
.NET Framework. Instale la versión necesaria de .NET Framework. El instalador de la versión
más reciente de .NET Framework está disponible en la página de descargas de .NET Core.
Si la aplicación se basa en la implementación autocontenida, incluirá el entorno de ejecución en la
implementación. No se requiere la instalación de ningún marco en el servidor.
5. Configure los puertos y las direcciones URL en 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 lo siguiente:
UseUrls
El argumento de la línea de comandos urls
La variable de entorno ASPNETCORE_URLS
UrlPrefixes
En el código siguiente, se muestra cómo usar UrlPrefixescon la dirección IP local del servidor,
10.0.0.4 , en el puerto 443:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.UrlPrefixes.Add("https://10.0.0.4:443");
});

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.
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 generan vulnerabilidades de seguridad en la aplicación. Esto
se aplica tanto a los caracteres comodín fuertes como a los débiles. Use nombres de host explícitos o
direcciones IP en lugar de caracteres comodín. Los enlaces de carácter comodín de subdominio (por
ejemplo, *.mysub.com ) no suponen ningún riesgo de seguridad si se controla todo el dominio primario (a
diferencia de *.com , que sí es vulnerable). Para obtener más información, vea RFC 7230: Sección 5.4: Host.

6. Registre previamente los prefijos de URL en el servidor.


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.
Use la herramienta netsh.exe para registrar URL de la aplicación:

netsh http add urlacl url=<URL> user=<USER>

<URL> : localizador uniforme de recursos (URL ) completo especificado. No use ningún enlace de
carácter comodín. Use un nombre de host válido o la dirección IP local. La dirección URL debe
incluir una barra diagonal final.
<USER> : especifica el nombre de usuario o de grupo de usuarios.
En el ejemplo siguiente, la dirección IP local del servidor es 10.0.0.4 :

netsh http add urlacl url=https://10.0.0.4:443/ user=Users

Cuando se registra una dirección URL, la herramienta responde con


URL reservation successfully added .

Para eliminar una dirección URL registrada, use el comando delete urlacl :

netsh http delete urlacl url=<URL>

7. Registre certificados X.509 en el servidor.


Use la herramienta netsh.exe para registrar certificados de la aplicación:

netsh http add sslcert ipport=<IP>:<PORT> certhash=<THUMBPRINT> appid="{<GUID>}"

<IP> : especifica la dirección IP local para el enlace. No use ningún enlace de carácter comodín.
Use una dirección IP válida.
<PORT> : especifica el puerto para el enlace.
<THUMBPRINT> : huella digital del certificado X.509.
<GUID> : GUID generado por el desarrollador para representar la aplicación con fines
informativos.
A modo de referencia, almacene el GUID en la aplicación como etiqueta de paquete:
En Visual Studio:
Abra las propiedades del proyecto de la aplicación. Para ello, haga clic con el botón
derecho en la aplicación, en el Explorador de soluciones, y seleccione Propiedades.
Seleccione la pestaña Paquete.
Escriba el GUID que creó en el campo Etiquetas.
Si no usa Visual Studio:
Abra el archivo de proyecto de la aplicación.
Agregue una propiedad <PackageTags> a un elemento <PropertyGroup> nuevo o
existente con el GUID que creó:

<PropertyGroup>
<PackageTags>9412ee86-c21b-4eb8-bd89-f650fbf44931</PackageTags>
</PropertyGroup>

En el ejemplo siguiente:
La dirección IP local del servidor es 10.0.0.4 .
Un generador de GUID aleatorios en línea proporciona el valor appid .

netsh http add sslcert


ipport=10.0.0.4:443
certhash=b66ee04419d4ee37464ab8785ff02449980eae10
appid="{9412ee86-c21b-4eb8-bd89-f650fbf44931}"

Cuando se registra un certificado, la herramienta responde con


SSL Certificate successfully added .

Para eliminar un registro de certificados, use el comando delete sslcert :

netsh http delete sslcert ipport=<IP>:<PORT>

Documentación de referencia de netsh.exe:


Comandos Netsh para protocolo de transferencia de hipertexto (HTTP )
Cadenas de UrlPrefix
8. Ejecutar la aplicación.
No se necesitan privilegios de administrador para ejecutar la aplicación al enlazar a localhost
mediante HTTP (no HTTPS ) con un número de puerto mayor que 1024. Para otras configuraciones
(por ejemplo, usar una dirección IP local o enlazar al puerto 443), ejecute la aplicación con
privilegios de administrador.
La aplicación responde a la dirección IP pública del servidor. En este ejemplo, el servidor está
disponible en Internet mediante su dirección IP pública, 104.214.79.47 .
En este ejemplo, se usa un certificado de desarrollo. La página se carga de forma segura tras omitir
la advertencia de que el certificado no es de confianza para el explorador.
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
Habilitación de la autenticación de Windows con HTTP.sys
API HTTP Server
Repositorio aspnet/HttpSysServer de GitHub (código fuente)
El host
Solución de problemas de proyectos de ASP.NET Core
Hospedaje de ASP.NET Core en un servicio de
Windows
02/07/2019 • 19 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 se inicia automáticamente después de reiniciar el
servidor.
Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos
ASP.NET Core SDK 2.1 o posterior
PowerShell 6.2 o posterior

Plantilla Worker Service


La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de
larga duración. Para usar la plantilla como base de una aplicación de servicio de Windows:
1. Cree una aplicación Worker Service con la plantilla de .NET Core.
2. Siga las instrucciones de la sección Configuración de aplicaciones para actualizar la aplicación Worker Service
a fin de que se ejecute como un servicio de Windows.
Visual Studio
Visual Studio Code y CLI de .NET Core
1. Cree un nuevo proyecto.
2. Seleccione Aplicación web de ASP.NET Core. Seleccione Siguiente.
3. Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado.
Seleccione Crear.
4. En el cuadro de diálogo Crear una aplicación web ASP.NET Core, confirme que las opciones .NET Core y
ASP.NET Core 3.0 estén seleccionadas.
5. Seleccione la plantilla Worker Service. Seleccione Crear.

Configuración de aplicaciones
Se llama a IHostBuilder.UseWindowsService , proporcionado por el paquete
Microsoft.Extensions.Hosting.WindowsServices, cuando se crea el host. Si la aplicación se ejecuta como un
servicio de Windows, el método:
Establece la vigencia del host en WindowsServiceLifetime .
Establece la raíz de contenido.
Habilita el registro en el registro de eventos con el nombre de la aplicación como nombre de origen
predeterminado.
El nivel de registro puede configurarse con la clave Logging:LogLevel:Default en el archivo
appsettings.Production.json.
Los administradores son los únicos que pueden crear nuevos orígenes de eventos. Cuando no se puede
crear un origen de eventos con el nombre de la aplicación, se registra una advertencia para el origen
Aplicación y los registros de eventos se deshabilitan.

public class Program


{
public static async Task Main(string[] args)
{
await CreateHostBuilder(args).Build().RunAsync();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ServiceA>();
services.AddHostedService<ServiceB>();
})
// Only required if the service responds to requests.
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

La aplicación requiere referencias de paquetes para Microsoft.AspNetCore.Hosting.WindowsServices y


Microsoft.Extensions.Logging.EventLog.
Para probar y depurar cuando se ejecuta fuera de un servicio, agregue código para determinar si la aplicación se
ejecuta como un servicio o una aplicación de consola. Compruebe si el depurador está asociado o hay presente un
conmutador --console . Si alguna de estas condiciones se cumple (la aplicación no se ejecuta como un servicio),
llame a Run. Si las condiciones no se cumplen (la aplicación se ejecuta como servicio):
Llame a SetCurrentDirectory y use una ruta de acceso a la ubicación de publicación de la aplicación. No llame
a GetCurrentDirectory para obtener la ruta de acceso porque una aplicación de servicio de Windows devuelve
una carpeta C:\WINDOWS\system32 cuando se llama a GetCurrentDirectory. Para obtener más información,
consulte la sección Directorio actual y raíz del contenido. Este paso se realiza antes de que la aplicación se
configure en CreateWebHostBuilder .
Llame a RunAsService para ejecutar la aplicación como un servicio.
Dado que el Proveedor de configuración de línea de comandos requiere pares nombre-valor en los argumentos
de línea de comandos, el conmutador --console se quita de los argumentos antes de que CreateDefaultBuilder
los reciba.
Para escribir en el registro de eventos de Windows, agregue el proveedor EventLog a ConfigureLogging.
Establezca el nivel de registro con la clave Logging:LogLevel:Default en el archivo appsettings.Production.json.
En el ejemplo siguiente de la aplicación de ejemplo, se llama a RunAsCustomService en lugar de a RunAsService
con el fin de controlar los eventos de duración dentro de la aplicación. Para obtener más información, consulte la
sección Control de los eventos de inicio y detención.
public class Program
{
public static void Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));

if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
Directory.SetCurrentDirectory(pathToContentRoot);
}

var builder = CreateWebHostBuilder(


args.Where(arg => arg != "--console").ToArray());

var host = builder.Build();

if (isService)
{
// To run the app without the CustomWebHostService change the
// next line to host.RunAsService();
host.RunAsCustomService();
}
else
{
host.Run();
}
}

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


WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddEventLog();
})
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseStartup<Startup>();
}

Tipo de implementación
Para obtener información y consejos sobre los escenarios de implementación, consulte Implementación de
aplicaciones .NET Core.
Implementación dependiente de marco (FDD)
La implementación dependiente de marco de trabajo (FDD ) se basa en la presencia de una versión compartida de
.NET Core en todo el sistema en el sistema de destino. Cuando se adopta el escenario FDD siguiendo las
instrucciones de este artículo, el SDK genera un archivo ejecutable ( .exe), denominado ejecutable dependiente del
marco.
Agregue los siguientes elementos de propiedad al archivo del proyecto:
<OutputType> – Tipo de salida de la aplicación ( Exe para ejecutable).
<LangVersion> – Versión del lenguaje C# ( latest o preview ).

No se requiere un archivo web.config, que normalmente se produce cuando se publica una aplicación ASP.NET
Core, para una aplicación de Windows Services. Para deshabilitar la creación de un archivo web.config agregue la
propiedad <IsTransformWebConfigDisabled> establecida en true .
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

El identificador en tiempo de ejecución (RID ) (<RuntimeIdentifier>) contiene la plataforma de destino. En el


ejemplo siguiente, el RID se establece en win7-x64 . La propiedad <SelfContained> se establece en false . Estas
propiedades indican al SDK que genere un archivo ejecutable ( .exe) para Windows y una aplicación que depende
del marco .NET Core compartido.
No se requiere un archivo web.config, que normalmente se produce cuando se publica una aplicación ASP.NET
Core, para una aplicación de Windows Services. Para deshabilitar la creación de un archivo web.config agregue la
propiedad <IsTransformWebConfigDisabled> establecida en true .

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

El identificador en tiempo de ejecución (RID ) (<RuntimeIdentifier>) contiene la plataforma de destino. En el


ejemplo siguiente, el RID se establece en win7-x64 . La propiedad <SelfContained> se establece en false . Estas
propiedades indican al SDK que genere un archivo ejecutable ( .exe) para Windows y una aplicación que depende
del marco .NET Core compartido.
La propiedad <UseAppHost> se establece en true . Esta propiedad proporciona el servicio con una ruta de acceso
de activación (un archivo ejecutable, .exe) para una FDD.
No se requiere un archivo web.config, que normalmente se produce cuando se publica una aplicación ASP.NET
Core, para una aplicación de Windows Services. Para deshabilitar la creación de un archivo web.config agregue la
propiedad <IsTransformWebConfigDisabled> establecida en true .

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
<UseAppHost>true</UseAppHost>
<SelfContained>false</SelfContained>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>

Implementación autocontenida (SCD)


Una implementación autocontenida (SCD ) no depende de la presencia de un marco compartido en el sistema
host. El tiempo de ejecución y las dependencias de la aplicación se implementan con la aplicación.
Un identificador en tiempo de ejecución (RID ) se incluye en el <PropertyGroup> que contiene la plataforma de
destino:

<RuntimeIdentifier>win7-x64</RuntimeIdentifier>

Para publicar para varios RID:


Proporcione los RID en una lista delimitada por punto y coma.
Utilice el nombre de propiedad <RuntimeIdentifiers> (en plural).
Para más información, vea el Catálogo de identificadores de entorno de ejecución (RID ) de .NET Core.
Una propiedad <SelfContained> se establece en true :

<SelfContained>true</SelfContained>

Cuenta de usuario de servicio


Para crear una cuenta de usuario para un servicio, use el cmdlet New -LocalUser de un shell de comandos
administrativos de PowerShell 6.
En la actualización de octubre de 2018 de Windows 10 (versión 1809/compilación 10.0.17763) o posterior:

New-LocalUser -Name {NAME}

En el sistema operativo Windows anterior a la actualización de octubre de 2018 de Windows 10


(versión 1809/compilación 10.0.17763):

powershell -Command "New-LocalUser -Name {NAME}"

Proporcione una contraseña segura cuando se le solicite.


A menos que el parámetro -AccountExpires se proporcione para el cmdlet New -LocalUser con una fecha de
expiración DateTime, la cuenta no expirará.
Para más información, vea Microsoft.PowerShell.LocalAccounts y Service User Accounts (Cuentas de usuario del
servicio).
Un enfoque alternativo de administración de usuarios al usar Active Directory consiste en usar cuentas de servicio
administradas. Para obtener más información, consulte grupo Managed Service Accounts Overview (Información
general sobre cuentas de servicio administradas de grupo).

Derechos para iniciar sesión como servicio


Para establecer los derechos de Iniciar sesión como servicio para una cuenta de usuario del servicio:
1. Abra el editor de la Directiva de seguridad local mediante la ejecución de secpol.msc.
2. Expanda el nodo Directivas locales y, después, seleccione Asignación de derechos de usuario.
3. Abra la directiva Iniciar sesión como servicio.
4. Seleccione Agregar usuario o grupo.
5. Proporcione el nombre de objeto (cuenta de usuario) mediante cualquiera de los métodos siguientes:
a. Escriba la cuenta de usuario ( {DOMAIN OR COMPUTER NAME\USER} ) en el campo del nombre de objeto y
seleccione Aceptar para agregar el usuario a la directiva.
b. Seleccione Opciones avanzadas. Seleccione Buscar ahora. Seleccione la cuenta de usuario de la lista.
Seleccione Aceptar. Seleccione Aceptar nuevo para agregar el usuario a la directiva.
6. Seleccione Aceptar o Aplicar para aceptar los cambios.

Creación y administración del servicio de Windows


Creación de un servicio
Use los comandos de PowerShell para registrar un servicio. Desde un shell de comandos administrativos de
PowerShell 6, ejecute los comandos siguientes:

$acl = Get-Acl "{EXE PATH}"


$aclRuleArgs = {DOMAIN OR COMPUTER NAME\USER}, "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit",
"None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs)
$acl.SetAccessRule($accessRule)
$acl | Set-Acl "{EXE PATH}"

New-Service -Name {NAME} -BinaryPathName {EXE FILE PATH} -Credential {DOMAIN OR COMPUTER NAME\USER} -
Description "{DESCRIPTION}" -DisplayName "{DISPLAY NAME}" -StartupType Automatic

{EXE PATH} – Ruta de acceso a la carpeta de la aplicación en el host (por ejemplo, d:\myservice ). No incluya el
archivo ejecutable de la aplicación en la ruta de acceso. No se requiere una barra diagonal al final.
{DOMAIN OR COMPUTER NAME\USER} –Cuenta de usuario de servicio (por ejemplo, Contoso\ServiceUser ).
{NAME} –Nombre de servicio (por ejemplo, MyService ).
{EXE FILE PATH} – Ruta de acceso del ejecutable de la aplicación (por ejemplo, d:\myservice\myservice.exe ).
Incluya el nombre de archivo del ejecutable con la extensión.
{DESCRIPTION} – Descripción del servicio (por ejemplo, My sample service ).
{DISPLAY NAME} – Nombre para mostrar del servicio (por ejemplo, My Service ).

Inicio de un servicio
Inicie el servicio con el siguiente comando de PowerShell 6:

Start-Service -Name {NAME}

Este comando tarda unos segundos en iniciar el servicio.


Determinación del estado de un servicio
Para comprobar el estado de un servicio, use el siguiente comando de PowerShell 6:

Get-Service -Name {NAME}

El estado se notifica como uno de los siguientes valores:


Starting
Running
Stopping
Stopped

Detención de un servicio
Detenga un servicio con el siguiente comando de PowerShell 6:

Stop-Service -Name {NAME}

Eliminación de un servicio
Tras un breve intervalo para detener un servicio, elimine el servicio con el siguiente comando de PowerShell 6:

Remove-Service -Name {NAME}

Control de los eventos de inicio y detención


Control de los eventos de inicio y detención
Para controlar los eventos OnStarting, OnStarted y OnStopping:
1. Cree una clase que se derive de WebHostService con los métodos OnStarting , OnStarted y OnStopping :

[DesignerCategory("Code")]
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.LogInformation("OnStarting method called.");
base.OnStarting(args);
}

protected override void OnStarted()


{
_logger.LogInformation("OnStarted method called.");
base.OnStarted();
}

protected override void OnStopping()


{
_logger.LogInformation("OnStopping method called.");
base.OnStopping();
}
}

2. Cree un método de extensión para IWebHost que pase CustomWebHostService a 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 método de extensión RunAsCustomService en lugar de RunAsService:

host.RunAsCustomService();

Para ver la ubicación de RunAsService en Program.Main , consulte el ejemplo de código que se muestra en
la sección Tipo de implementación.

Escenarios de servidor proxy y equilibrador de carga


Los servicios que interactúan con las solicitudes de Internet o de una red corporativa y están detrás de un proxy o
de un equilibrador de carga podrían requerir configuración adicional. Para más información, consulte
Configuración de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga.

Configuración de HTTPS
Para configurar el servicio con un punto de conexión seguro:
1. Cree un certificado X.509 para el sistema de hospedaje mediante la adquisición del certificado de la
plataforma y con mecanismos de implementación.
2. Especifique una configuración de punto de conexión HTTPS de servidor Kestrel para usar el certificado.
No se admite el uso del certificado de desarrollo HTTPS de ASP.NET Core para proteger un punto de conexión
de servicio.

Directorio actual y raíz del contenido


El directorio de trabajo actual devuelto por una llamada a 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). Utilice uno de los siguientes enfoques para mantener
los archivos de configuración y los activos de un servicio, así como para acceder a ellos.
Uso de ContentRootPath o ContentRootFileProvider
Use IHostEnvironment.ContentRootPath o ContentRootFileProvider para buscar los recursos de una aplicación.
Configuración de la ruta de acceso raíz del contenido en la carpeta de la aplicación
ContentRootPath es la misma ruta de acceso proporcionada al argumento binPath cuando se crea un servicio. En
lugar de llamar GetCurrentDirectory para crear rutas de acceso a los archivos de configuración, llame a
SetCurrentDirectory con la ruta de acceso a la raíz del contenido de la aplicación.
En Program.Main , determine la ruta de acceso a la carpeta del archivo ejecutable del servicio y use la ruta de
acceso para establecer la raíz del contenido de la aplicación:

var pathToExe = Process.GetCurrentProcess().MainModule.FileName;


var pathToContentRoot = Path.GetDirectoryName(pathToExe);
Directory.SetCurrentDirectory(pathToContentRoot);

CreateWebHostBuilder(args)
.Build()
.RunAsService();

Almacenamiento de los archivos de un servicio en una ubicación adecuada en el disco


Especifique una ruta de acceso absoluta con SetBasePath al utilizar un IConfigurationBuilder 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 genérico de .NET
Solución de problemas de proyectos de ASP.NET Core
Kestrel: configuración de punto de conexión (configuración de HTTPS y compatibilidad de SNI incluidas)
Host web de ASP.NET Core
Solución de problemas de proyectos de ASP.NET Core
Hospedar ASP.NET Core en Linux con Nginx
10/05/2019 • 27 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.
Si la aplicación se ejecuta localmente y no está configurada para realizar conexiones seguras (HTTPS ), adopte
cualquiera de los métodos siguientes:
Configure la aplicación para controlar las conexiones locales seguras. Para obtener más información, vea la
sección Configuración de HTTPS.
Quite https://localhost:5001 (si existe) de la propiedad applicationUrl en el archivo
Properties/launchSettings.json.
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 ). Es habitual encontrar las aplicaciones web en el directorio var
(por ejemplo, var/www/helloapp).

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.
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 reducir las cargas de trabajo, por ejemplo, suministrar contenido estático,
almacenar solicitudes en caché, comprimir solicitudes y finalizar HTTPS 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.
Invoque el método UseForwardedHeaders en Startup.Configure antes de llamar a UseAuthentication o 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 middleware, los encabezados
predeterminados para reenviar son None .
Los servidores proxy que se ejecutan en direcciones de bucle invertido (127.0.0.0/8, [:: 1]), incluida la dirección
de localhost (127.0.0.1) estándar, son de confianza de forma predeterminada. Si otras redes o servidores proxy
de confianza de la organización tramitan solicitudes entre Internet y el servidor web, agréguelos a la lista de
KnownProxies o KnownNetworks con ForwardedHeadersOptions. En el ejemplo siguiente se agrega un
servidor proxy de confianza en la dirección IP 10.0.0.100 al middleware de encabezados reenviados
KnownProxies en Startup.ConfigureServices :

services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

Para obtener más información, vea Configuración de 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. Siga las instrucciones de instalación para Ubuntu en Nginx: Official
Debian/Ubuntu packages (Nginx: paquetes oficiales de Debian y 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: Endpoint configuration (Kestrel: configuración de los puntos 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 la aplicación: dotnet <app_assembly.dll> , donde app_assembly.dll es el nombre de archivo de
ensamblado de la aplicación.
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-helloapp.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/www/helloapp
ExecStart=/usr/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
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.
Use TimeoutStopSec para configurar el período de tiempo que hay que esperar para que la aplicación se apague
después de que reciba la señal de interrupción inicial. Si la aplicación no se apaga en este período, se emite
SIGKILL para terminar la aplicación. Proporcione el valor como segundos sin unidad (por ejemplo, 150 ), un
intervalo de tiempo (por ejemplo, 2min 30s ) o infinity para deshabilitar el tiempo de expiración. El valor
predeterminado de TimeoutStopSec es el valor de DefaultTimeoutStopSec del archivo de configuración del
administrador (systemd -system.conf, system.conf.d, systemd -user.conf o user.conf.d). El tiempo de expiración
predeterminado para la mayoría de las distribuciones es de 90 segundos.

# The default value is 90 seconds for most distributions.


TimeoutStopSec=90

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

No se admiten los separadores de dos puntos ( : ) en los nombres de variable de entorno. Use un subrayado
doble ( __ ) en lugar de dos puntos. El proveedor de configuración de variables de entorno convierte doble
subrayado en dos puntos cuando las variables de entorno se leen en la configuración. En el ejemplo siguiente, la
clave de la cadena de conexión ConnectionStrings:DefaultConnection se establece en el archivo de definición de
servicio como ConnectionStrings__DefaultConnection :

Environment=ConnectionStrings__DefaultConnection={Connection String}

Guarde el archivo y habilite el servicio.

sudo systemctl enable kestrel-helloapp.service


Inicie el servicio y compruebe que se está ejecutando.

sudo systemctl start kestrel-helloapp.service


sudo systemctl status kestrel-helloapp.service

● kestrel-helloapp.service - Example .NET Web API App running on Ubuntu


Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
CGroup: /system.slice/kestrel-helloapp.service
└─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.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

Visualización de 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-helloapp.service , use el
siguiente comando:

sudo journalctl -fu kestrel-helloapp.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-helloapp.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

Campos del encabezado de solicitud más largos


Si la aplicación requiere campos de encabezado de solicitud más largos que los permitidos por la configuración
predeterminada del servidor proxy (normalmente 4K u 8K, según la plataforma), será necesario ajustar las
siguientes directivas. Los valores aplicables dependerán del escenario. Para obtener más información, consulte
la documentación del servidor.
proxy_buffer_size
proxy_buffers
proxy_busy_buffers_size
large_client_header_buffers

WARNING
No aumente los valores predeterminados de los búferes de proxy a menos que sea necesario. El aumento de estos valores
incrementa el riesgo de saturación del búfer (desbordamiento) y ataques por denegación de servicio (DoS) realizados por
usuarios malintencionados.

Protección de la nube
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

Protección de 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.
Configuración de HTTPS
Configuración de la aplicación para conexiones locales seguras (HTTPS )
El comando dotnet run usa el archivo Properties/launchSettings.json de la aplicación, que configura la aplicación
para que escuche en las direcciones URL proporcionadas por la propiedad applicationUrl (por ejemplo,
https://localhost:5001; http://localhost:5000 ).

Configure la aplicación para que use un certificado en el desarrollo para el comando dotnet run o el entorno de
desarrollo (F5 o CTRL+F5 en Visual Studio Code) mediante uno de los siguientes enfoques:
Reemplace el certificado predeterminado de configuración (recomendado)
KestrelServerOptions.ConfigureHttpsDefaults
Configure el proxy inverso para conexiones de cliente seguras (HTTPS )
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 son a través de HTTPS.
Si se va a deshabilitar HTTPS en el futuro, no agregue el encabezado HSTS, o bien elija un valor max-age
adecuado.
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, 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.
Para mitigar los ataques de secuestro de clic:
1. Edite el archivo nginx.conf:

sudo nano /etc/nginx/nginx.conf

Agregue la línea add_header X-Frame-Options "SAMEORIGIN"; .


2. Guarde el archivo.
3. 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 de código binario: paquetes
oficiales de Debian y Ubuntu)
Solución de problemas de proyectos de ASP.NET Core
Configuración de 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
05/07/2019 • 24 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 el servidor Kestrel. La extensión mod_proxy
y los módulos relacionados crean el proxy inverso del servidor.

Requisitos previos
Servidor que ejecute CentOS 7, con una cuenta de usuario estándar con privilegios sudo.
Tener .NET Core Runtime instalado en el servidor.
1. Vaya a la página de descargas de .NET Core.
2. Seleccione el tiempo de ejecución más reciente que no sea versión preliminar en la lista Runtime.
3. Seleccione y siga las instrucciones relativas a CentOS/Oracle.
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.
Si la aplicación se ejecuta localmente y no está configurada para realizar conexiones seguras (HTTPS ), adopte
cualquiera de los métodos siguientes:
Configure la aplicación para controlar las conexiones locales seguras. Para obtener más información, vea la
sección Configuración de HTTPS.
Quite https://localhost:5001 (si existe) de la propiedad applicationUrl en el archivo
Properties/launchSettings.json.
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 ). Es habitual encontrar las aplicaciones web en el directorio var
(por ejemplo, var/www/helloapp).

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.
Invoque el método UseForwardedHeaders en Startup.Configure antes de llamar a UseAuthentication o 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 middleware, los encabezados


predeterminados para reenviar son None .
Los servidores proxy que se ejecutan en direcciones de bucle invertido (127.0.0.0/8, [:: 1]), incluida la dirección
de localhost (127.0.0.1) estándar, son de confianza de forma predeterminada. Si otras redes o servidores proxy
de confianza de la organización tramitan solicitudes entre Internet y el servidor web, agréguelos a la lista de
KnownProxies o KnownNetworks con ForwardedHeadersOptions. En el ejemplo siguiente se agrega un
servidor proxy de confianza en la dirección IP 10.0.0.100 al middleware de encabezados reenviados
KnownProxies en Startup.ConfigureServices :

services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

Para más información, consulte Configuración de 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 helloapp.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}helloapp-error.log
CustomLog ${APACHE_LOG_DIR}helloapp-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: Endpoint configuration
(Kestrel: configuración de los puntos 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-helloapp.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/www/helloapp
ExecStart=/usr/local/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=apache
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

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 de los archivos.
Use TimeoutStopSec para configurar el período de tiempo que hay que esperar para que la aplicación se apague
después de que reciba la señal de interrupción inicial. Si la aplicación no se apaga en este período, se emite
SIGKILL para terminar la aplicación. Proporcione el valor como segundos sin unidad (por ejemplo, 150 ), un
intervalo de tiempo (por ejemplo, 2min 30s ) o infinity para deshabilitar el tiempo de expiración. El valor
predeterminado de TimeoutStopSec es el valor de DefaultTimeoutStopSec del archivo de configuración del
administrador (systemd -system.conf, system.conf.d, systemd -user.conf o user.conf.d). El tiempo de expiración
predeterminado para la mayoría de las distribuciones es de 90 segundos.

# The default value is 90 seconds for most distributions.


TimeoutStopSec=90

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>"

No se admiten los separadores de dos puntos ( : ) en los nombres de variable de entorno. Use un subrayado
doble ( __ ) en lugar de dos puntos. El proveedor de configuración de variables de entorno convierte doble
subrayado en dos puntos cuando las variables de entorno se leen en la configuración. En el ejemplo siguiente, la
clave de la cadena de conexión ConnectionStrings:DefaultConnection se establece en el archivo de definición de
servicio como ConnectionStrings__DefaultConnection :

Environment=ConnectionStrings__DefaultConnection={Connection String}

Guarde el archivo y habilite el servicio.

sudo systemctl enable kestrel-helloapp.service

Inicie el servicio y compruebe que se está ejecutando:

sudo systemctl start kestrel-helloapp.service


sudo systemctl status kestrel-helloapp.service

● kestrel-helloapp.service - Example .NET Web API App running on CentOS 7


Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
CGroup: /system.slice/kestrel-helloapp.service
└─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.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

Visualización de 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-helloapp.service , use el siguiente
comando:

sudo journalctl -fu kestrel-helloapp.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-helloapp.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 nube
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 HTTPS
Configuración de la aplicación para conexiones locales seguras (HTTPS )
El comando dotnet run usa el archivo Properties/launchSettings.json de la aplicación, que configura la aplicación
para que escuche en las direcciones URL proporcionadas por la propiedad applicationUrl (por ejemplo,
https://localhost:5001; http://localhost:5000 ).

Configure la aplicación para que use un certificado en el desarrollo para el comando dotnet run o el entorno de
desarrollo (F5 o CTRL+F5 en Visual Studio Code) mediante uno de los siguientes enfoques:
Reemplace el certificado predeterminado de configuración (recomendado)
KestrelServerOptions.ConfigureHttpsDefaults
Configure el proxy inverso para conexiones de cliente seguras (HTTPS )
Para configurar Apache para HTTPS, 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 HTTPS, instale el módulo mod_rewrite para habilitar la reescritura de direcciones URL:

sudo yum install mod_rewrite

Modifique el archivo helloapp.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/helloapp-error.log
CustomLog /var/log/httpd/helloapp-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.
Para mitigar los ataques de secuestro de clic:
1. Edite el archivo httpd.conf.
sudo nano /etc/httpd/conf/httpd.conf

Agregue la línea Header append X-FRAME-OPTIONS "SAMEORIGIN" .


2. Guarde el archivo.
3. Reinicie Apache.
Examen de tipo MIME
El encabezado X-Content-Type-Optionsimpide que Internet Explorer examine MIME (de forma que el elemento
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 helloapp
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/helloapp-error.log
CustomLog /var/log/httpd/helloapp-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>

Campos del encabezado de solicitud más largos


Si la aplicación requiere campos de encabezado de solicitud más largos que los permitidos por la configuración
predeterminada del servidor proxy (normalmente 8190 bytes), ajuste el valor de la directiva
LimitRequestFieldSize. El valor aplicable dependerá del escenario. Para obtener más información, consulte la
documentación del servidor.
WARNING
No aumente el valor predeterminado de LimitRequestFieldSize a menos que sea necesario. El aumento de este valor
incrementa el riesgo de saturación del búfer (desbordamiento) y ataques por denegación de servicio (DoS) realizados por
usuarios malintencionados.

Recursos adicionales
Requisitos previos para .NET Core en Linux
Solución de problemas de proyectos de ASP.NET Core
Configuración de ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
Hospedar ASP.NET Core en contenedores de Docker
10/05/2019 • 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.
Imágenes de Docker para ASP.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.
Imágenes de Docker para ASP.NET Core
20/06/2019 • 9 minutes to read • Edit Online

En este tutorial se muestra cómo ejecutar una aplicación ASP.NET Core en contenedores de Docker.
En este tutorial ha:
Obtener información sobre las imágenes de Microsoft .NET Core Docker
Descargará una aplicación de ejemplo de ASP.NET Core
Ejecutará la aplicación de ejemplo localmente
Ejecutará la aplicación de ejemplo en contenedores de Linux
Ejecutará la aplicación de ejemplo en contenedores de Windows
Realizará compilaciones e implementaciones manualmente

Imágenes de Docker para ASP.NET Core


En este tutorial, descargará una aplicación de ejemplo de ASP.NET Core y la ejecutará en contenedores de Docker.
El ejemplo funciona con contenedores de Linux y Windows.
El Dockerfile de ejemplo usa la característica de compilación en varias fases de Docker para la compilación y
ejecución en distintos contenedores. Los contenedores de compilación y ejecución se crean a partir de imágenes
que proporciona Microsoft en Docker Hub:
dotnet/core/sdk

En el ejemplo se usa esta imagen para compilar la aplicación. La imagen contiene el SDK de .NET Core, que
incluye las herramientas de línea de comandos (CLI). Esta imagen está optimizada para el desarrollo local,
la depuración y las pruebas unitarias. Las herramientas instaladas para el desarrollo y la compilación hacen
que esta sea una imagen relativamente grande.
dotnet/core/aspnet

En el ejemplo se usa esta imagen para ejecutar la aplicación. La imagen contiene el entorno de ejecución y
las bibliotecas de ASP.NET Core y está optimizada para la ejecución de aplicaciones en producción.
Diseñada para acelerar la implementación y el inicio de las aplicaciones, la imagen es relativamente
pequeña, de forma que se optimiza el rendimiento de la red desde Docker Registry hasta el host de Docker.
Solo se copian en el contenedor los archivos binarios y el contenido necesario para ejecutar una aplicación.
El contenido está listo para ejecutarse, lo que permite el tiempo más rápido desde Docker run hasta el
inicio de la aplicación. En el modelo de Docker no se necesita compilación de código dinámico.

Requisitos previos
SDK de .NET Core 2.2
Cliente de Docker 18.03 o posterior
Distribuciones de Linux
CentOS
Debian
Fedora
Ubuntu
macOS
Windows
Git

Descarga de la aplicación de ejemplo


Para descargar el ejemplo, clone el repositorio de Docker de .NET Core:

git clone https://github.com/dotnet/dotnet-docker

Probar la aplicación localmente


Vaya a la carpeta de proyecto en dotnet-docker/samples/aspnetapp/aspnetapp.
Ejecute el siguiente comando para compilar y ejecutar localmente la aplicación:

dotnet run

Vaya a http://localhost:5000 en un explorador para probar la aplicación.


Presione Ctrl+C en el símbolo del sistema para detener la aplicación.

Ejecución en un contenedor de Linux


En el cliente de Docker, cambie a contenedores de Linux.
Vaya a la carpeta de Dockerfile en dotnet-docker/samples/aspnetapp.
Ejecute los siguientes comandos para compilar y ejecutar el ejemplo en Docker:

docker build -t aspnetapp .


docker run -it --rm -p 5000:80 --name aspnetcore_sample aspnetapp

Los argumentos del comando build :


Asigne a la imagen el nombre aspnetapp.
Busque el archivo Dockerfile en la carpeta actual (el punto al final).
Los argumentos del comando de ejecución:
Asigne un pseudo-TTY y manténgalo abierto aunque no esté asociado. (El mismo efecto que
--interactive --tty ).
Quite automáticamente el contenedor cuando se cierre.
Asigne al puerto 5000 de la máquina local el puerto 80 en el contenedor.
Asigne al contenedor el nombre aspnetcore_sample.
Especifique la imagen aspnetapp.
Vaya a http://localhost:5000 en un explorador para probar la aplicación.

Ejecución en un contenedor de Windows


En el cliente de Docker, cambie a los contenedores de Windows.
Vaya a la carpeta de archivos de Docker en dotnet-docker/samples/aspnetapp .
Ejecute los siguientes comandos para compilar y ejecutar el ejemplo en Docker:

docker build -t aspnetapp .


docker run -it --rm --name aspnetcore_sample aspnetapp

Para contenedores de Windows, necesita la dirección IP del contenedor (no sirve ir a


http://localhost:5000 ):

Abra otro símbolo del sistema.


Ejecute docker ps para ver los contenedores en ejecución. Compruebe que aparece el contenedor
"aspnetcore_sample".
Ejecute docker exec aspnetcore_sample ipconfig para mostrar la dirección IP del contenedor. La
salida del comando es similar a este ejemplo:

Ethernet adapter Ethernet:

Connection-specific DNS Suffix . : contoso.com


Link-local IPv6 Address . . . . . : fe80::1967:6598:124:cfa3%4
IPv4 Address. . . . . . . . . . . : 172.29.245.43
Subnet Mask . . . . . . . . . . . : 255.255.240.0
Default Gateway . . . . . . . . . : 172.29.240.1

Copie la dirección IPv4 (por ejemplo, 172.29.245.43) del contenedor y péguela en la barra de direcciones
del explorador para probar la aplicación.

Compilaciones e implementaciones manuales


En algunos escenarios, puede que quiera implementar una aplicación en un contenedor mediante la copia de los
archivos de aplicación que son necesarios en tiempo de ejecución. En esta sección se muestra cómo realizar una
implementación manual.
Vaya a la carpeta de proyecto en dotnet-docker/samples/aspnetapp/aspnetapp.
Ejecute el comando dotnet publish:

dotnet publish -c Release -o published

Los argumentos del comando:


Compile la aplicación en modo de versión (el valor predeterminado es modo de depuración).
Cree los archivos en la carpeta publicada.
Ejecute la aplicación.
Windows:

dotnet published\aspnetapp.dll

Linux:

dotnet published/aspnetapp.dll

Vaya a http://localhost:5000 para ver la página principal.


El archivo Dockerfile
Aquí el archivo Dockerfile se usa con el comando docker build que ejecutó anteriormente. Se usa dotnet publish
de la misma manera que en esta sección para realizar compilaciones e implementaciones.

FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build


WORKDIR /app

# copy csproj and restore as distinct layers


COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app


COPY aspnetapp/. ./aspnetapp/
WORKDIR /app/aspnetapp
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS runtime


WORKDIR /app
COPY --from=build /app/aspnetapp/out ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]

Recursos adicionales
Comando de compilación de Docker
Comando de ejecución de Docker
Ejemplo de Docker de ASP.NET Core (el usado en este tutorial).
Configurar ASP.NET Core para trabajar con servidores proxy y equilibradores de carga
Working with Visual Studio Docker Tools (Trabajo con Visual Studio Docker Tools)
Depuración con Visual Studio Code

Pasos siguientes
En este tutorial ha:
Obtener información sobre las imágenes de Microsoft .NET Core Docker
Descargado una aplicación de ejemplo de ASP.NET Core
Ejecutado la aplicación de ejemplo localmente
Ejecutado la aplicación de ejemplo en contenedores de Linux
Ejecutado el ejemplo con contenedores de Windows
Realizado compilaciones e implementaciones manualmente
El repositorio de Git que contiene la aplicación de ejemplo también incluye documentación. Para información
general de los recursos disponibles en el repositorio, consulte el archivo Léame. En concreto, vea cómo
implementar HTTPS:
Developing ASP.NET Core Applications with Docker over HTTPS (Desarrollo de aplicaciones ASP.NET Core con
Docker a través de HTTPS )
Visual Studio Tools para Docker con ASP.NET Core
21/05/2019 • 19 minutes to read • Edit Online

En Visual Studio 2017 y versiones posteriores, se pueden 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 2019 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 Desktop for Windows: What to know before you
install (Docker Desktop para Windows: información previa a 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 mcr.microsoft.com/dotnet/core/aspnet:2.1 AS base
WORKDIR /app
EXPOSE 59518
EXPOSE 44364

FROM mcr.microsoft.com/dotnet/core/sdk:2.1 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. Se ofrecen dos opciones diferentes: 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.

Si quiere un comportamiento diferente basado en la configuración de compilación (por ejemplo, Debug o Release),
agregue archivos docker-compose específicos de la configuración. Los nombres de los archivos deben basarse en
la configuración de compilación (por ejemplo, docker-compose.vs.debug.yml y docker-compose.vs.release.yml) y
deben colocarse en la misma ubicación que el archivo docker-compose-override.yml.
Con los archivos de invalidación específicos de la configuración, puede especificar distintos valores de
configuración (por ejemplo, variables de entorno o puntos de entrada) para las configuraciones de compilación de
depuración y lanzamiento.
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
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
Desarrollo de contenedores con Visual Studio
Azure Service Fabric: Preparación del entorno de desarrollo
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
18/06/2019 • 27 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 integración con IIS cuando la aplicación se hospeda fuera de proceso 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 de la integración con IIS al hospedar fuera de proceso, 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 ForwardedHeadersOptions en Startup.ConfigureServices o directamente en el
método de extensión con UseForwardedHeaders, los encabezados predeterminados para reenviar son
ForwardedHeaders.None. La propiedad ForwardedHeaders debe estar configurada con los encabezados que se van
a reenviar.

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 obtener más información, consulte NGINX: Using the Forwarded header (NGINX:
uso del encabezado Forwarded).

Configuración de Apache
X-Forwarded-For se agrega automáticamente. Consulte 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
IList<string> vacío.
OPCIÓN DESCRIPCIÓN

ForwardedForHeaderName Use el encabezado especificado por


esta propiedad en lugar del
especificado por
ForwardedHeadersDefaults.XForwar
dedForHeaderName. 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 ForwardedHeaders.XForwardedProt


procesar. Consulte o".
ForwardedHeaders Enum para
obtener la lista de campos que se El valor predeterminado es
aplican. Los valores típicos ForwardedHeaders.None.
asignados a esta propiedad son
"ForwardedHeaders.XForwardedFor

ForwardedHostHeaderName Use el encabezado especificado por


esta propiedad en lugar del
especificado por
ForwardedHeadersDefaults.XForwar
dedHostHeaderName. 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.XForwar
dedProtoHeaderName. 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.
OPCIÓN DESCRIPCIÓN

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.RemoteIpA
ddress. 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 un
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.RemoteIpA
ddress. 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 un
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.XOriginal
ForHeaderName.

De manera predeterminada, es
X-Original-For .

OriginalHostHeaderName Use el encabezado especificado por


esta propiedad en lugar del
especificado por
ForwardedHeadersDefaults.XOriginal
ForHeaderName.

De manera predeterminada, es
X-Original-Host .

OriginalProtoHeaderName Use el encabezado especificado por


esta propiedad en lugar del
especificado por
ForwardedHeadersDefaults.XOriginal
ProtoHeaderName.

De manera predeterminada, es
X-Original-Proto .

RequireHeaderSymmetry Requiere que el número de valores


de encabezado esté sincronizado
entre los valores
ForwardedHeadersOptions.Forwarde
dHeaders 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 /fooes la ruta de acceso base de aplicación para una ruta de acceso de proxy que se pasa como
/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 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));
});

Reenvío del esquema para servidores proxy inversos Linux y que


no son de IIS
Las plantillas de .NET Core llaman a UseHttpsRedirection y UseHsts. Estos métodos incluyen un sitio en
un bucle infinito si se implementan en una instancia de Azure App Service de Linux, una máquina virtual
Linux de Azure (VM ), o bien detrás de cualquier otro servidor proxy inverso, además de IIS. El servidor
proxy inverso termina TLS y Kestrel no es consciente del esquema de solicitud correcto. En esta
configuración también se produce un error de OAuth y OIDC, ya que generan redirecciones incorrectas.
UseIISIntegration agrega y configura middleware de encabezados reenviados cuando se ejecuta detrás
de IIS, pero no hay ninguna configuración automática coincidente para Linux (integración de Apache o
Nginx).
Para reenviar el esquema desde el servidor proxy en escenarios que no sean de IIS, agregue y configure
Middleware de encabezados reenviados. En Startup.ConfigureServices , use el código siguiente:

// using Microsoft.AspNetCore.HttpOverrides;

if (string.Equals(
Environment.GetEnvironmentVariable("ASPNETCORE_FORWARDEDHEADERS_ENABLED"),
"true", StringComparison.OrdinalIgnoreCase))
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
// Only loopback proxies are allowed by default.
// Clear that restriction because forwarders are enabled by explicit
// configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
}

Hasta que se proporcionen nuevas imágenes de contenedor en Azure, tendrá que crear una configuración
de aplicación (variable de entorno) para ASPNETCORE_FORWARDEDHEADERS_ENABLED establecida en true . Para
más información, vea Templates do not work in Antares Linux due to missing scheme forwarders
(aspnet/AspNetCore #4135) (En Linux Antares no funcionan las plantillas debido a la ausencia de
reenviadores de esquema (aspnet/AspNetCore #4135)).

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. Use middleware insertado para escribir encabezados de solicitud en la respuesta
de una aplicación o para registrar los encabezados.
Para escribir los encabezados en la respuesta de la aplicación, coloque el siguiente middleware insertado
terminal inmediatamente después de la llamada a UseForwardedHeaders en Startup.Configure :

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

Puede escribir en registros en lugar de en el cuerpo de respuesta. Así el sitio funcionará con normalidad
durante la depuración.
Para ello, siga estos pasos:
Inserte ILogger<Startup> en la clase Startup como se describe en Creación de registros durante el
inicio.
Coloque el siguiente software intermedio insertado inmediatamente después de la llamada a
UseForwardedHeaders en Startup.Configure .
app.Use(async (context, next) =>
{
// Request method, scheme, and path
_logger.LogDebug("Request Method: {METHOD}", context.Request.Method);
_logger.LogDebug("Request Scheme: {SCHEME}", context.Request.Scheme);
_logger.LogDebug("Request Path: {PATH}", context.Request.Path);

// Headers
foreach (var header in context.Request.Headers)
{
_logger.LogDebug("Header: {KEY}: {VALUE}", header.Key, header.Value);
}

// Connection: RemoteIp
_logger.LogDebug("Request RemoteIp: {REMOTE_IP_ADDRESS}",
context.Connection.RemoteIpAddress);

await next();
});

Si se procesa, los valores X-Forwarded-{For|Proto|Host} se trasladan a X-Original-{For|Proto|Host} . 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. El valor predeterminado de
ForwardLimit es 1 (uno), por lo que solo el valor más a la derecha de los encabezados se procesa, a
menos que el valor de ForwardLimit aumente.
La dirección IP remota original de la solicitud debe coincidir con una entrada de las listas KnownProxies o
KnownNetworks antes de procesar los encabezados reenviados. Esto limita la suplantación de encabezados
al no aceptarse reenviadores de servidores proxy que no son de confianza. Cuando se detecta un servidor
proxy desconocido, el registro indica la dirección de dicho proxy:

September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321

En el ejemplo anterior, 10.0.0.100 es un servidor proxy. Si se trata de un servidor proxy de confianza,


agregue la dirección IP del servidor a KnownProxies o agregue una red de confianza a KnownNetworks en
Startup.ConfigureServices . Para más información, vea la sección Opciones del Middleware de
encabezados reenviados.

services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

IMPORTANT
Admita solo las redes y los servidores proxy de confianza para reenviar encabezados. De lo contrario, se pueden
producir ataques de suplantación de IP.

Reenvío de certificados
En Azure
Vea la documentación de Azure para configurar Azure Web Apps. En el método Startup.Configure de la
aplicación, agregue el código siguiente antes de la llamada a app.UseAuthentication(); :
app.UseCertificateForwarding();

También tendrá que configurar el middleware de reenvío de certificados para especificar el nombre de
encabezado que usa Azure. En el método Startup.ConfigureServices de la aplicación, agregue el código
siguiente para configurar el encabezado desde el que el middleware crea un certificado:

services.AddCertificateForwarding(options =>
options.CertificateHeader = "X-ARR-ClientCert");

Con otros servidores proxy web


Si usa un proxy que no sea IIS o Enrutamiento de solicitud de aplicaciones de Azure Web Apps, configure
el proxy para reenviar el certificado que ha recibido en un encabezado HTTP. En el método
Startup.Configure de la aplicación, agregue el código siguiente antes de la llamada a
app.UseAuthentication(); :

app.UseCertificateForwarding();

También tendrá que configurar el middleware de reenvío de certificados para especificar el nombre de
encabezado. En el método Startup.ConfigureServices de la aplicación, agregue el código siguiente para
configurar el encabezado desde el que el middleware crea un certificado:

services.AddCertificateForwarding(options =>
options.CertificateHeader = "YOUR_CERTIFICATE_HEADER_NAME");

Por último, si el proxy realiza una operación que no sea la codificación Base64 del certificado (como
sucede con Nginx), establezca la opción HeaderConverter . Considere el ejemplo siguiente de
Startup.ConfigureServices :

services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
options.HeaderConverter = (headerValue) =>
{
var clientCertificate =
/* some conversion logic to create an X509Certificate2 */
return clientCertificate;
}
});

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
22/05/2019 • 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.
Implementar aplicaciones 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 Almacenamiento en caché en ASP.NET Core
distribuido.

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, vea 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 Almacenamiento en caché en ASP.NET Core
distribuido).

Para más información, consulte Estado de sesión y aplicación:


Estado de la 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: Estado de la sesión).

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
Protección y almacenamiento en caché de datos
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
Almacenamiento en caché en ASP.NET Core distribuido.

Obtención de datos de aplicaciones


Si las aplicaciones de la granja de servidores web son capaces de responder a solicitudes, obtenga solicitudes,
conexiones y datos adicionales de las aplicaciones mediante el middleware en línea del terminal. Para obtener
más información y un código de ejemplo, vea Solución de problemas de proyectos de ASP.NET Core.
Perfiles de publicación de Visual Studio para la
implementación de aplicaciones ASP.NET Core
03/07/2019 • 24 minutes to read • Edit Online

Por Sayed Ibrahim Hashimi y Rick Anderson


Este documento se centra en el uso de Visual Studio 2017 o versiones posteriores 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. 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 comando dotnet new mvc produce un archivo de proyecto que contiene el siguiente elemento de nivel
superior <Project> :

<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- omitted for brevity -->
</Project>

El atributo Sdk del elemento anterior <Project> importa las propiedades y objetivos de
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.props y
$ (MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets, respectivamente. La ubicación predeterminada
de $(MSBuildSDKsPath) (con Visual Studio 2019 Enterprise) es la carpeta %programfiles(x86 )%\Microsoft Visual
Studio\2019\Enterprise\MSBuild\Sdks.
Microsoft.NET.Sdk.Web (SDK web) depende de otros SDK, incluidos Microsoft.NET.Sdk (SDK de .NET Core) y
Microsoft.NET.Sdk.Razor ( SDK de Razor ). Se importan las propiedades y objetivos de MSBuild asociados con
cada SDK dependiente. 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 procesan los elementos del proyecto de MSBuild (archivos). El tipo de elemento
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 los patrones wwwroot\** , **\*.config y **\*.json se
incluyen en la lista de elementos Content . Por ejemplo, el patrón global wwwroot\** coincide con los archivos de
la carpeta wwwroot y de sus subcarpetas y.
El SDK web importa el SDK de Razor. Como resultado, los archivos que coinciden con los patrones **\*.cshtml
y **\*.razor se incluyen también en la lista de elementos Content .
El SDK web importa el SDK de Razor. Como resultado, los archivos que coinciden con el patrón **\*.cshtml y
se incluye también en la lista de elementos Content .
Para agregar explícitamente un archivo a la lista de publicación, agregue el archivo directamente en el archivo
.csproj, como se muestra en la sección 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 Visual Studio: Se han restaurado 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:

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 {version} for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Restore completed in 36.81 ms for C:\Webs\Web1\Web1.csproj.


Web1 -> C:\Webs\Web1\bin\Debug\netcoreapp{X.Y}\Web1.dll
Web1 -> C:\Webs\Web1\bin\Debug\netcoreapp{X.Y}\Web1.Views.dll
Web1 -> C:\Webs\Web1\bin\Debug\netcoreapp{X.Y}\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
netcoreapp{X.Y} .

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 o versiones posteriores para crear un perfil de publicación. Una vez
creado el perfil, es posible realizar la publicación 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 proyecto, 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 MSBuild denominado
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. Este
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 aspnet/websdk repository.
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}

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>

En el ejemplo anterior, <LastUsedBuildConfiguration> se establece 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
En el ejemplo siguiente se utiliza una aplicación web ASP.NET Core de ejemplo creada mediante Visual Studio y
denominada AzureWebApp. Se agrega un perfil de publicación de aplicaciones de Azure con Visual Studio. Para
obtener más información sobre cómo crear un perfil, consulte la sección Perfiles de publicación.
Para implementar la aplicación con un perfil de publicación, ejecute el comando msbuild desde un símbolo del
sistema para desarrolladores de Visual Studio. El símbolo del sistema se encuentra disponible en la carpeta
Visual Studio del menú Inicio, en la barra de tareas de Windows. Para facilitar el acceso, puede agregar el
símbolo del sistema al menú Herramientas de Visual Studio. Para más información, consulte Símbolo del
sistema para desarrolladores de Visual Studio.
MSBuild usa la sintaxis de comando siguiente:

msbuild {PATH}
/p:DeployOnBuild=true
/p:PublishProfile={PROFILE}
/p:Username={USERNAME}
/p:Password={PASSWORD}

{RUTA DE ACCESO }: ruta de acceso al archivo de proyecto de la aplicación.


{PERFIL }: nombre del perfil de publicación.
{NOMBRE DE USUARIO }: nombre de usuario de MSDeploy. El valor {NOMBRE DE USUARIO } se
encuentra en el perfil de publicación.
{CONTRASEÑA}: contraseña de MSDeploy. Puede obtener el valor {CONTRASEÑA} en el archivo
{PERFIL }.PublishSettings. Descargue el archivo .PublishSettings desde:
Explorador de soluciones: Seleccione Ver > Cloud Explorer. Conéctese con su suscripción de Azure.
Abra App Services. Haga clic con el botón derecho en la aplicación. Seleccione Descargar perfil de
publicación.
Azure Portal: Haga clic en Obtener perfil de publicación, en el panel Información general de la
aplicación web.
En el ejemplo siguiente se usa un perfil de publicación denominado AzureWebApp - Web Deploy:

msbuild "AzureWebApp.csproj"
/p:DeployOnBuild=true
/p:PublishProfile="AzureWebApp - Web Deploy"
/p:Username="$AzureWebApp"
/p:Password=".........."

También se puede usar un perfil de publicación ejecutando el comando dotnet msbuild de la CLI de .NET Core
en un símbolo del sistema de Windows:

dotnet msbuild "AzureWebApp.csproj"


/p:DeployOnBuild=true
/p:PublishProfile="AzureWebApp - Web Deploy"
/p:Username="$AzureWebApp"
/p:Password=".........."
NOTE
El comando dotnet msbuild es multiplataforma y permite compilar aplicaciones ASP.NET Core en macOS y Linux. Sin
embargo, MSBuild en macOS y Linux no permite implementar una aplicación en Azure ni en otro punto de conexión de
MSDeploy. MSDeploy solo está disponible en Windows.

Establecimiento del entorno


Incluya la propiedad <EnvironmentName> en el perfil de publicación ( .pubxml) o el archivo del proyecto para
configurar el entorno de la aplicación:

<PropertyGroup>
<EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

Si necesita transformaciones de web.config (por ejemplo, establecer variables de entorno basadas en la


configuración, el perfil o el entorno), consulte Transformación de web.config.

Archivos de exclusión
Al publicar aplicaciones web de ASP.NET Core, se incluyen los siguientes recursos:
Artefactos de compilación
Las carpetas y archivos que coinciden con los siguientes patrones globales de uso:
**\*.config (por ejemplo, web.config )
**\*.json (por ejemplo, appsettings.json)
wwwroot\**

MSBuild admite los patrones globales. Por ejemplo, el siguiente elemento <Content> suprime la copia de los
archivos de texto ( .txt) de la carpeta wwwroot/content y de 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
En el marcado siguiente:
Incluye una carpeta images fuera del directorio del proyecto a la carpeta wwwroot/images del sitio de
publicación.
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.
<ItemGroup>
<_CustomFiles Include="$(MSBuildProjectDirectory)/../images/**/*" />
<DotnetPublishFiles Include="@(_CustomFiles)">
<DestinationRelativePath>wwwroot/images/%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</DotnetPublishFiles>
</ItemGroup>

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>

Consulte el archivo Léame del repositorio 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.
Repositorio de SDK web en GitHub: 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
Transformación de web.config
Estructura de directorios de ASP.NET Core
20/06/2019 • 4 minutes to read • Edit Online

Por Luke Latham


El directorio publish contiene recursos de la aplicación producidos por el comando dotnet publish que se
pueden implementar. El directorio contiene lo siguiente:
Archivos de aplicación
Archivos de configuración
Recursos estáticos
Paquetes
Entorno de ejecución (solo implementación autocontenida)

TIPO DE APLICACIÓN ESTRUCTURA DE DIRECTORIOS

Implementación dependiente de marco publish†


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 DE ENSAMBLADO}.deps.json
{NOMBRE DE ENSAMBLADO}.dll
{NOMBRE DE ENSAMBLADO}.pdb
{NOMBRE DE ENSAMBLADO}.Views.dll
{NOMBRE DE ENSAMBLADO}.Views.pdb
{NOMBRE DE
ENSAMBLADO}.runtimeconfig.json
web.config (implementaciones de IIS)

Implementación independiente publish†


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 DE ENSAMBLADO}.deps.json
{NOMBRE DE ENSAMBLADO}.dll
{NOMBRE DE ENSAMBLADO}.exe
{NOMBRE DE ENSAMBLADO}.pdb
{NOMBRE DE ENSAMBLADO}.Views.dll
{NOMBRE DE ENSAMBLADO}.Views.pdb
{NOMBRE DE
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.
Crear una carpeta Logs es útil para el registro de depuración mejorado del módulo de ASP.NET Core. El módulo
no crea automáticamente las carpetas de la ruta de acceso proporcionada al valor <handlerSetting> , que deben
existir previamente en la implementación para permitir que el módulo escriba el registro de depuración.
Se puede crear un directorio Logs 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. 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.

Recursos adicionales
dotnet publish
Implementación de aplicaciones .NET Core
Marcos de trabajo de destino
Catálogo de identificadores de entorno de ejecución (RID ) de .NET Core
Comprobaciones de estado en ASP.NET Core
17/05/2019 • 40 minutes to read • Edit Online

Por Luke Latham y Glenn Condron


ASP.NET Core ofrece el Middleware de comprobaciones de estado y bibliotecas para informar sobre el estado de
los componentes de la infraestructura de la aplicación.
Una aplicación se encarga de exponer las comprobaciones de estado como puntos de conexión HTTP. Los puntos
de conexión de las comprobaciones de estado pueden configurarse para diversos escenarios de supervisión en
tiempo real:
Los orquestadores de contenedores y los equilibradores de carga pueden utilizar los sondeos de estado para
comprobar el estado de una aplicación. Por ejemplo, para responder a una comprobación de estado con
errores, es posible que un orquestador de contenedores detenga una implementación en curso o reinicie un
contenedor. Como respuesta a una aplicación con estado incorrecto, es posible que un equilibrador de carga
enrute el tráfico al margen de la instancia con errores hacia una instancia con estado correcto.
El uso de la memoria, el disco y otros recursos del servidor físico puede supervisarse para determinar si el
estado es correcto.
Las comprobaciones de estado pueden probar las dependencias de una aplicación, como las bases de datos y
los puntos de conexión de servicio externo, para confirmar la disponibilidad y el funcionamiento normal.
Vea o descargue el código de ejemplo (cómo descargarlo)
La aplicación de muestra incluye ejemplos de los escenarios descritos en este tema. Para ejecutar la aplicación de
ejemplo para un escenario determinado, use el comando dotnet run desde la carpeta del proyecto en un shell de
comandos. Para obtener información sobre cómo utilizar la aplicación de ejemplo, consulte el archivo
README.md de la aplicación de ejemplo y las descripciones de escenarios de este tema.

Requisitos previos
Normalmente, las comprobaciones de estado se usan con un servicio de supervisión externa o un orquestador de
contenedores para comprobar el estado de una aplicación. Antes de agregar comprobaciones de estado a una
aplicación, debe decidir en qué sistema de supervisión se va a usar. El sistema de supervisión determina qué tipos
de comprobaciones de estado se deben crear y cómo configurar sus puntos de conexión.
Haga referencia al metapaquete Microsoft.AspNetCore.App o agregue una referencia de paquete al paquete
Microsoft.AspNetCore.Diagnostics.HealthChecks.
La aplicación de ejemplo proporciona código de inicio para mostrar las comprobaciones de estado para varios
escenarios. En el escenario de sondeo de base de datos se comprueba el estado de una conexión de base de datos
mediante AspNetCore.Diagnostics.HealthChecks. El escenario sondeo de DbContext comprueba una base de
datos mediante un elemento DbContext de EF Core. Para explorar los escenarios de la base de datos, la aplicación
de ejemplo:
Crea una base de datos y proporciona su cadena de conexión en el archivo appsettings.json.
Tiene las siguientes referencias de paquete en su archivo de proyecto:
AspNetCore.HealthChecks.SqlServer
Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
NOTE
AspNetCore.Diagnostics.HealthChecks es un puerto de BeatPulse y Microsoft no lo mantiene ni lo admite.

Otro escenario de comprobación de estado muestra cómo filtrar las comprobaciones de estado por un puerto de
administración. La aplicación de ejemplo requiere la creación de un archivo Properties/launchSettings.json que
incluya la dirección URL de administración y el puerto de administración. Para obtener más información, consulte
la sección Filtrado por puerto.

Sondeo de estado básico


Para muchas aplicaciones, una configuración de sondeo de estado básico que notifique la disponibilidad de la
aplicación para procesar las solicitudes (ejecución) es suficiente para detectar el estado de la aplicación.
La configuración básica registra los servicios de comprobación de estado y llama al Middleware de
comprobaciones de estado para responder a un punto de conexión de dirección URL con una respuesta de
estado. De forma predeterminada, no se registran comprobaciones de estado específicas para probar cualquier
dependencia o subsistema concretos. La aplicación se considera correcta si es capaz de responder en la dirección
URL de punto de conexión de estado. El escritor de respuesta predeterminado escribe el estado (HealthStatus)
como respuesta de texto no cifrado al cliente, que indica un estado HealthStatus.Healthy, HealthStatus.Degraded
o HealthStatus.Unhealthy.
Registre los servicios de comprobación de estado con AddHealthChecks de Startup.ConfigureServices . Agregue
el Middleware de comprobaciones de estado con UseHealthChecks en la canalización de procesamiento de
solicitudes de Startup.Configure .
En la aplicación de ejemplo, el punto de conexión de la comprobación de estado se crea en /health
(BasicStartup.cs):

public class BasicStartup


{
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health");

app.Run(async (context) =>


{
await context.Response.WriteAsync(
"Navigate to /health to see the health status.");
});
}
}

Para ejecutar el escenario de configuración básica mediante la aplicación de ejemplo, ejecute el comando
siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario basic

Ejemplo de Docker
Docker ofrece una directiva de HEALTHCHECK integrada que puede utilizarse para comprobar el estado de una
aplicación que use la configuración de comprobación de estado básica:
HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit

Creación de comprobaciones de estado


Las comprobaciones de estado se crean mediante la implementación de la interfaz de IHealthCheck. El método
CheckHealthAsync devuelve un elemento Task< HealthCheckResult > que indica el estado como Healthy ,
Degraded o Unhealthy . El resultado se escribe como una respuesta de texto no cifrado con un código de estado
configurable. (La configuración se describe en la sección Opciones de comprobación de estado).
HealthCheckResult también puede devolver pares clave-valor opcionales.
Comprobación de estado de ejemplo
La siguiente clase ExampleHealthCheck muestra el diseño de una comprobación de estado:

public class ExampleHealthCheck : IHealthCheck


{
public ExampleHealthCheck()
{
// Use dependency injection (DI) to supply any required services to the
// health check.
}

public Task<HealthCheckResult> CheckHealthAsync(


HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
// Execute health check logic here. This example sets a dummy
// variable to true.
var healthCheckResultHealthy = true;

if (healthCheckResultHealthy)
{
return Task.FromResult(
HealthCheckResult.Healthy("The check indicates a healthy result."));
}

return Task.FromResult(
HealthCheckResult.Unhealthy("The check indicates an unhealthy result."));
}
}

Registro de los servicios de comprobación de estado


El tipo ExampleHealthCheck se agrega a los servicios de comprobación de estado con AddCheck:

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>("example_health_check");
}

La sobrecarga AddCheck que se muestra en el ejemplo siguiente establece el estado de error (HealthStatus) para
notificar cuándo la comprobación de estado informa de un error. Si el estado de error se establece en null (valor
predeterminado), HealthStatus.Unhealthy se notifica. Esta sobrecarga es un escenario útil para los creadores de
bibliotecas en los que la aplicación ejecuta el estado de error indicado por la biblioteca cuando se produce un
error de comprobación de estado si la implementación de la comprobación de estado respeta la configuración.
Las etiquetas pueden usarse para filtrar las comprobaciones de estado, que se describen con más detalle en la
sección Filtrado de las comprobaciones de estado.
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>(
"example_health_check",
failureStatus: HealthStatus.Degraded,
tags: new[] { "example" });

AddCheck también puede ejecutar una función lambda. En el ejemplo siguiente, el nombre de la comprobación
de estado se especifica como Example y la comprobación siempre devuelve un estado correcto:

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddCheck("Example", () =>
HealthCheckResult.Healthy("Example is OK!"), tags: new[] { "example" })
}

Uso del Middleware de comprobaciones de estado


En Startup.Configure , llame a UseHealthChecks en la canalización de procesamiento con la dirección URL del
punto de conexión o la ruta de acceso relativa:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health");
}

Si las comprobaciones de estado deben realizar la escucha en un puerto específico, use una sobrecarga de
UseHealthChecks para establecer el puerto, como se describe con más detalle en la sección Filtrado por puerto:

app.UseHealthChecks("/health", port: 8000);

El Middleware de comprobaciones de estado es un middleware terminal en la canalización de procesamiento de


solicitudes de la aplicación. El primer punto de conexión de comprobación de estado encontrado que sea una
coincidencia exacta con la dirección URL de la solicitud ejecuta y cortocircuita el resto de la canalización de
middleware. Cuando se produce el cortocircuito, no se ejecuta ningún middleware que siga la comprobación de
estado coincidente.

Opciones de comprobación de estado


El elemento HealthCheckOptions ofrece una oportunidad para personalizar el comportamiento de las
comprobaciones de estado:
Filtrado de las comprobaciones de estado
Personalización del código de estado HTTP
Supresión de los encabezados de caché
Personalización del resultado
Filtrado de las comprobaciones de estado
De forma predeterminada, el Middleware de comprobaciones de estado ejecuta todas las comprobaciones de
estado registradas. Para ejecutar un subconjunto de comprobaciones de estado, proporcione una función que
devuelva un valor booleano para la opción Predicate. En el ejemplo siguiente, la etiqueta ( bar_tag ) de la
comprobación de estado Bar la filtra, en la instrucción condicional de la función, donde true solo se devuelve si
la propiedad Tags de la comprobación de estado coincide con foo_tag o baz_tag :
using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddCheck("Foo", () =>
HealthCheckResult.Healthy("Foo is OK!"), tags: new[] { "foo_tag" })
.AddCheck("Bar", () =>
HealthCheckResult.Unhealthy("Bar is unhealthy!"), tags: new[] { "bar_tag" })
.AddCheck("Baz", () =>
HealthCheckResult.Healthy("Baz is OK!"), tags: new[] { "baz_tag" });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// Filter out the 'Bar' health check. Only Foo and Baz execute.
Predicate = (check) => check.Tags.Contains("foo_tag") ||
check.Tags.Contains("baz_tag")
});
}

Personalización del código de estado HTTP


Use ResultStatusCodes para personalizar la asignación del estado de mantenimiento de los códigos de estado
HTTP. Las siguientes asignaciones de StatusCodes son los valores predeterminados que el middleware utiliza.
Cambie los valores de código de estado para satisfacer sus necesidades.

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// The following StatusCodes are the default assignments for
// the HealthStatus properties.
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
}
});
}

Supresión de los encabezados de caché


AllowCachingResponses controla si el Middleware de comprobaciones de estado agrega encabezados HTTP a
una respuesta de sondeo para evitar el almacenamiento en caché de respuesta. Si el valor es false (valor
predeterminado), el middleware establece o invalida los encabezados Cache-Control , Expires y Pragma para
evitar el almacenamiento en caché de respuesta. Si el valor es true , el middleware no modifica los encabezados
de caché de la respuesta.
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// The default value is false.
AllowCachingResponses = false
});
}

Personalización del resultado


La opción ResponseWriter obtiene o establece un delegado que se usa para escribir la respuesta. El delegado
predeterminado escribe una respuesta de texto no cifrado mínima con el valor de cadena HealthReport.Status.

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// WriteResponse is a delegate used to write the response.
ResponseWriter = WriteResponse
});
}

private static Task WriteResponse(HttpContext httpContext,


HealthReport result)
{
httpContext.Response.ContentType = "application/json";

var json = new JObject(


new JProperty("status", result.Status.ToString()),
new JProperty("results", new JObject(result.Entries.Select(pair =>
new JProperty(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description),
new JProperty("data", new JObject(pair.Value.Data.Select(
p => new JProperty(p.Key, p.Value))))))))));
return httpContext.Response.WriteAsync(
json.ToString(Formatting.Indented));
}

Sondeo de bases de datos


Una comprobación de estado puede especificar que una consulta de base de datos se ejecute como una prueba
booleana para indicar si esta responde con normalidad.
En la aplicación de ejemplo se usa AspNetCore.Diagnostics.HealthChecks, una biblioteca de comprobaciones de
estado para las aplicaciones ASP.NET Core, que permite realizar una comprobación de estado en una base de
datos de SQL Server. AspNetCore.Diagnostics.HealthChecks ejecuta una consulta SELECT 1 en la base de datos
para confirmar que la conexión a la base de datos es correcta.
WARNING
Al comprobar la conexión de una base de datos a una consulta, elija una consulta que se devuelva rápidamente. El enfoque
de la consulta plantea el riesgo de sobrecargar la base de datos y degradar el rendimiento. En la mayoría de los casos, no es
necesario ejecutar una consulta de prueba. Simplemente, realizar una conexión correcta a la base de datos es suficiente. Si
resulta necesario ejecutar una consulta, elija una consulta SELECT sencilla, como SELECT 1 .

Incluya una referencia de paquete a AspNetCore.HealthChecks.SqlServer.


Proporcione una cadena de conexión a base de datos válida en el archivo appsettings.json de la aplicación de
ejemplo. La aplicación usa una base de datos de SQL Server denominada HealthCheckSample :

{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;Conn
ectRetryCount=0"
},
"Logging": {
"LogLevel": {
"Default": "Debug"
},
"Console": {
"IncludeScopes": "true"
}
}
}

Registre los servicios de comprobación de estado con AddHealthChecks de Startup.ConfigureServices . La


aplicación de ejemplo llama al método AddSqlServer con la cadena de conexión de la base de datos
(DbHealthStartup.cs):

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
}

Llame al Middleware de comprobaciones de estado en la canalización de procesamiento de la aplicación en


Startup.Configure :

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health");

Para ejecutar el escenario de sondeo de base de datos mediante la aplicación de ejemplo, ejecute el comando
siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario db

NOTE
AspNetCore.Diagnostics.HealthChecks es un puerto de BeatPulse y Microsoft no lo mantiene ni lo admite.
Sondeo de DbContext de Entity Framework Core
La comprobación DbContext confirma que la aplicación puede comunicarse con la base de datos configurada
para un elemento DbContext de EF Core. La comprobación DbContext se admite en las aplicaciones que:
Usan Entity Framework (EF ) Core.
Incluyen una referencia de paquete a Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.
AddDbContextCheck<TContext> registra una comprobación de estado para un elemento DbContext . El elemento
DbContext se proporciona como TContext en el método. Hay disponible una sobrecarga para configurar el
estado de error, las etiquetas y una consulta de prueba personalizada.
De manera predeterminada:
DbContextHealthCheck llama al método CanConnectAsync de EF Core. Se puede personalizar qué operación se
ejecuta al comprobar el estado con sobrecargas del método AddDbContextCheck .
El nombre de la comprobación de estado es el nombre del tipo TContext .

En la aplicación de ejemplo, AppDbContext se proporciona para AddDbContextCheck y se registra como un servicio


en Startup.ConfigureServices .
DbContextHealthStartup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();

services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(
Configuration["ConnectionStrings:DefaultConnection"]);
});
}

En la aplicación de ejemplo, UseHealthChecks agrega el Middleware de comprobaciones de estado en


Startup.Configure .

DbContextHealthStartup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseHealthChecks("/health");

Para ejecutar el escenario de sondeo de DbContext mediante la aplicación de ejemplo, confirme que la base de
datos que la cadena de conexión especifica no exista en la instancia de SQL Server. Si la base de datos existe,
elimínela.
Ejecute el comando siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario dbcontext

Cuando la aplicación ya se esté ejecutando, compruebe el estado de mantenimiento mediante una solicitud al
punto de conexión /health en un explorador. La base de datos y AppDbContext no existen, por lo que la
aplicación proporciona la respuesta siguiente:
Unhealthy

Active la aplicación de ejemplo para crear la base de datos. Realice una solicitud a /createdatabase . La aplicación
responde:

Creating the database...


Done!
Navigate to /health to see the health status.

Realice una solicitud al punto de conexión /health . La base de datos y el contexto existen, por lo que la
aplicación responde:

Healthy

Active la aplicación de ejemplo para eliminar la base de datos. Realice una solicitud a /deletedatabase . La
aplicación responde:

Deleting the database...


Done!
Navigate to /health to see the health status.

Realice una solicitud al punto de conexión /health . La aplicación proporciona una respuesta incorrecta:

Unhealthy

Sondeos de preparación y ejecución independientes


En algunos escenarios de hospedaje, se usa un par de comprobaciones de estado que distinguen los dos estados
de la aplicación:
La aplicación está funcionando, pero aún no está preparada para recibir solicitudes. Este estado es la
preparación de la aplicación.
La aplicación está funcionando y responde a las solicitudes. Este estado es la ejecución de la aplicación.
Normalmente, la comprobación de preparación realiza un conjunto más amplio de comprobaciones, que
requieren mucho tiempo, para determinar si están disponibles todos los subsistemas y los recursos de la
aplicación. Una comprobación de ejecución simplemente realiza una comprobación rápida para determinar si la
aplicación está disponible para procesar las solicitudes. Después de que la aplicación pase la comprobación de
preparación, no es necesario sobrecargar aún más la aplicación con el conjunto amplio de comprobaciones de
preparación; en las comprobaciones siguientes solo se requiere comprobar la ejecución.
La aplicación de ejemplo contiene una comprobación de estado para notificar la finalización de la tarea de inicio
de ejecución prolongada en un servicio hospedado. El elemento StartupHostedServiceHealthCheck expone una
propiedad, StartupTaskCompleted , que el servicio hospedado puede establecer en true al terminar su tarea de
ejecución prolongada (StartupHostedServiceHealthCheck.cs):
public class StartupHostedServiceHealthCheck : IHealthCheck
{
private volatile bool _startupTaskCompleted = false;

public string Name => "slow_dependency_check";

public bool StartupTaskCompleted


{
get => _startupTaskCompleted;
set => _startupTaskCompleted = value;
}

public Task<HealthCheckResult> CheckHealthAsync(


HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
if (StartupTaskCompleted)
{
return Task.FromResult(
HealthCheckResult.Healthy("The startup task is finished."));
}

return Task.FromResult(
HealthCheckResult.Unhealthy("The startup task is still running."));
}
}

Un servicio hospedado (Services/StartupHostedService) se encarga de iniciar la tarea en segundo plano de larga


ejecución. Al finalizar la tarea, StartupHostedServiceHealthCheck.StartupTaskCompleted se establece en true :
public class StartupHostedService : IHostedService, IDisposable
{
private readonly int _delaySeconds = 15;
private readonly ILogger _logger;
private readonly StartupHostedServiceHealthCheck _startupHostedServiceHealthCheck;

public StartupHostedService(ILogger<StartupHostedService> logger,


StartupHostedServiceHealthCheck startupHostedServiceHealthCheck)
{
_logger = logger;
_startupHostedServiceHealthCheck = startupHostedServiceHealthCheck;
}

public Task StartAsync(CancellationToken cancellationToken)


{
_logger.LogInformation($"Startup Background Service is starting.");

// Simulate the effect of a long-running startup task.


Task.Run(async () =>
{
await Task.Delay(_delaySeconds * 1000);

_startupHostedServiceHealthCheck.StartupTaskCompleted = true;

_logger.LogInformation($"Startup Background Service has started.");


});

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)


{
_logger.LogInformation("Startup Background Service is stopping.");

return Task.CompletedTask;
}

public void Dispose()


{
}
}

La comprobación de estado se registra con AddCheck en Startup.ConfigureServices junto con el servicio


hospedado. Dado que el servicio hospedado debe establecer la propiedad en la comprobación de estado, esta
también se registra en el contenedor de servicios (LivenessProbeStartup.cs):
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
.AddCheck<StartupHostedServiceHealthCheck>(
"hosted_service_startup",
failureStatus: HealthStatus.Degraded,
tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(2);
options.Predicate = (check) => check.Tags.Contains("ready");
});

// The following workaround permits adding an IHealthCheckPublisher


// instance to the service container when one or more other hosted
// services have already been added to the app. This workaround
// won't be required with the release of ASP.NET Core 3.0. For more
// information, see: https://github.com/aspnet/Extensions/issues/639.
services.TryAddEnumerable(
ServiceDescriptor.Singleton(typeof(IHostedService),
typeof(HealthCheckPublisherOptions).Assembly
.GetType(HealthCheckServiceAssembly)));

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
}

Llame al Middleware de comprobaciones de estado en la canalización de procesamiento de la aplicación en


Startup.Configure . En la aplicación de ejemplo, se crean puntos de conexión de la comprobación de estado en
/health/ready para la comprobación de preparación y en /health/live para la comprobación de ejecución. La
comprobación de preparación filtra las comprobaciones de estado con la etiqueta ready . La comprobación de
ejecución filtra el elemento StartupHostedServiceHealthCheck y devuelve false en
HealthCheckOptions.Predicate. Para obtener más información, consulte Filtrado de las comprobaciones de
estado:

public void Configure(IApplicationBuilder app)


{
// The readiness check uses all registered checks with the 'ready' tag.
app.UseHealthChecks("/health/ready", new HealthCheckOptions()
{
Predicate = (check) => check.Tags.Contains("ready"),
});

app.UseHealthChecks("/health/live", new HealthCheckOptions()


{
// Exclude all checks and return a 200-Ok.
Predicate = (_) => false
});

Para ejecutar el escenario de configuración de la preparación/ejecución mediante la aplicación de ejemplo, ejecute


el comando siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario liveness

En un explorador, visite /health/ready varias veces hasta que hayan pasado 15 segundos. La comprobación de
estado notifica un estado Incorrecto durante los primeros 15 segundos. Pasados 15 segundos, el punto de
conexión notifica un estado Correcto, lo que indica que el servicio hospedado ya ha finalizado la tarea de
ejecución prolongada.
En este ejemplo también se crea un publicador de la comprobación de estado (IHealthCheckPublisher
implementación) que ejecuta la primera comprobación de preparación con un retraso de dos segundos. Para
obtener más información, consulte la sección Publicador de la comprobación de estado.
Ejemplo de Kubernetes
Utilizar comprobaciones de preparación y ejecución independientes es útil en un entorno como Kubernetes. En
Kubernetes, es posible que una aplicación deba realizar un trabajo de inicio que requiera mucho tiempo antes de
aceptar solicitudes, como una prueba de la disponibilidad de la base de datos subyacente. El hecho de utilizar
comprobaciones independientes permite que el orquestador distinga si la aplicación está funcionando, pero aún
no esté preparada, o si la aplicación no se ha podido iniciar. Para obtener más información sobre los sondeos de
preparación y ejecución en Kubernetes, consulte Configuración de sondeos de preparación y ejecución en la
documentación de Kubernetes.
En el ejemplo siguiente, se muestra una configuración de sondeo de preparación de Kubernetes:

spec:
template:
spec:
readinessProbe:
# an http probe
httpGet:
path: /health/ready
port: 80
# length of time to wait for a pod to initialize
# after pod startup, before applying health checking
initialDelaySeconds: 30
timeoutSeconds: 1
ports:
- containerPort: 80

Sondeo basado en métrica con un escritor de respuesta personalizada


La aplicación de ejemplo muestra una comprobación de estado de memoria con un escritor de respuesta
personalizada.
MemoryHealthCheck notifica un estado degradado si la aplicación usa más de un umbral de memoria determinado
(1 GB en la aplicación de ejemplo). El elemento HealthCheckResult incluye información del recolector de
elementos no utilizados (GC ) de la aplicación (MemoryHealthCheck.cs):
public class MemoryHealthCheck : IHealthCheck
{
private readonly IOptionsMonitor<MemoryCheckOptions> _options;

public MemoryHealthCheck(IOptionsMonitor<MemoryCheckOptions> options)


{
_options = options;
}

public string Name => "memory_check";

public Task<HealthCheckResult> CheckHealthAsync(


HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var options = _options.Get(context.Registration.Name);

// Include GC information in the reported diagnostics.


var allocated = GC.GetTotalMemory(forceFullCollection: false);
var data = new Dictionary<string, object>()
{
{ "AllocatedBytes", allocated },
{ "Gen0Collections", GC.CollectionCount(0) },
{ "Gen1Collections", GC.CollectionCount(1) },
{ "Gen2Collections", GC.CollectionCount(2) },
};

var status = (allocated < options.Threshold) ?


HealthStatus.Healthy : HealthStatus.Unhealthy;

return Task.FromResult(new HealthCheckResult(


status,
description: "Reports degraded status if allocated bytes " +
$">= {options.Threshold} bytes.",
exception: null,
data: data));
}
}

Registre los servicios de comprobación de estado con AddHealthChecks de Startup.ConfigureServices . En lugar


de pasar la comprobación de estado a AddCheck para habilitarla, MemoryHealthCheck se registra como servicio.
Todos los servicios registrados de IHealthCheck están disponibles para los servicios de comprobación de estado
y middleware. Se recomienda registrar los servicios de comprobación de estado como los servicios de Singleton.
CustomWriterStartup.cs:

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks()
.AddMemoryHealthCheck("memory");
}

Llame al Middleware de comprobaciones de estado en la canalización de procesamiento de la aplicación en


Startup.Configure . Se proporciona un delegado de WriteResponse a la propiedad ResponseWriter para generar
una respuesta JSON personalizada al ejecutarse la comprobación de estado:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
// This custom writer formats the detailed status as JSON.
ResponseWriter = WriteResponse
});

El método WriteResponse da a CompositeHealthCheckResult el formato de objeto JSON y suspende el resultado


de JSON para la respuesta de comprobación de estado:

private static Task WriteResponse(HttpContext httpContext,


HealthReport result)
{
httpContext.Response.ContentType = "application/json";

var json = new JObject(


new JProperty("status", result.Status.ToString()),
new JProperty("results", new JObject(result.Entries.Select(pair =>
new JProperty(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description),
new JProperty("data", new JObject(pair.Value.Data.Select(
p => new JProperty(p.Key, p.Value))))))))));
return httpContext.Response.WriteAsync(
json.ToString(Formatting.Indented));
}

Para ejecutar el sondeo basado en métrica con el resultado de un escritor de respuesta personalizada mediante la
aplicación de ejemplo, ejecute el comando siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario writer

NOTE
AspNetCore.Diagnostics.HealthChecks incluye escenarios de comprobación de estado basados en métrica, como
comprobaciones de almacenamiento del disco y de ejecución del máximo valor.
AspNetCore.Diagnostics.HealthChecks es un puerto de BeatPulse y Microsoft no lo mantiene ni lo admite.

Filtrado por puerto


Una llamada a UseHealthChecks con un puerto restringe las solicitudes de comprobación de estado al puerto
especificado. Esto se usa normalmente en un entorno de contenedor para exponer un puerto para los servicios de
supervisión.
La aplicación de ejemplo configura el puerto con el proveedor de configuración de variable de entorno. El puerto
se establece en el archivo launchSettings.json y se pasa al proveedor de configuración a través de una variable de
entorno. También debe configurar el servidor para que escuche las solicitudes en el puerto de administración.
Para utilizar la aplicación de ejemplo para que muestre la configuración del puerto de administración, cree el
archivo launchSettings.json en una carpeta Propiedades.
El siguiente archivo launchSettings.json no se incluye en los archivos de proyecto de la aplicación de ejemplo y
debe crearse manualmente.
Properties/launchSettings.json:
{
"profiles": {
"SampleApp": {
"commandName": "Project",
"commandLineArgs": "",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/",
"ASPNETCORE_MANAGEMENTPORT": "5001"
},
"applicationUrl": "http://localhost:5000/"
}
}
}

Registre los servicios de comprobación de estado con AddHealthChecks de Startup.ConfigureServices . La


llamada a UseHealthChecks especifica el puerto de administración ( ManagementPortStartup.cs):

public class ManagementPortStartup


{
public ManagementPortStartup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
services.AddHealthChecks();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{

app.UseHealthChecks("/health", port: Configuration["ManagementPort"]);

app.Run(async (context) =>


{
await context.Response.WriteAsync(
"Navigate to " +
$"http://localhost:{Configuration["ManagementPort"]}/health " +
"to see the health status.");
});
}
}
NOTE
Para evitar la creación del archivo launchSettings.json en la aplicación de ejemplo, configure las direcciones URL y el puerto
de administración explícitamente en código. En Program.cs, donde se crea WebHostBuilder, agregue una llamada a UseUrls
y proporcione el punto de conexión de respuesta normal de la aplicación y el punto de conexión del puerto de
administración. En ManagementPortStartup.cs, donde se llama a UseHealthChecks, especifique explícitamente el puerto de
administración.
Program.cs:

return new WebHostBuilder()


.UseConfiguration(config)
.UseUrls("http://localhost:5000/;http://localhost:5001/")
.ConfigureLogging(builder =>
{
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddConfiguration(config);
builder.AddConsole();
})
.UseKestrel()
.UseStartup(startupType)
.Build();

ManagementPortStartup.cs:

app.UseHealthChecks("/health", port: 5001);

Para ejecutar el escenario de configuración del puerto de administración mediante la aplicación de ejemplo,
ejecute el comando siguiente desde la carpeta del proyecto en un shell de comandos:

dotnet run --scenario port

Distribución de una biblioteca de comprobación de estado


Para distribuir una comprobación de estado como una biblioteca, haga lo siguiente:
1. Escriba una comprobación de estado que implemente la interfaz de IHealthCheck como una clase
independiente. La clase puede depender de la inserción de dependencias (DI), de la activación del tipo y de
las opciones denominadas para acceder a los datos de configuración.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace SampleApp
{
public class ExampleHealthCheck : IHealthCheck
{
private readonly string _data1;
private readonly int? _data2;

public ExampleHealthCheck(string data1, int? data2)


{
_data1 = data1 ?? throw new ArgumentNullException(nameof(data1));
_data2 = data2 ?? throw new ArgumentNullException(nameof(data2));
}

public async Task<HealthCheckResult> CheckHealthAsync(


HealthCheckContext context, CancellationToken cancellationToken)
{
try
{
// Health check logic
//
// data1 and data2 are used in the method to
// run the probe's health check logic.

// Assume that it's possible for this health check


// to throw an AccessViolationException.

return HealthCheckResult.Healthy();
}
catch (AccessViolationException ex)
{
return new HealthCheckResult(
context.Registration.FailureStatus,
description: "An access violation occurred during the check.",
exception: ex,
data: null);
}
}
}
}

2. Escriba un método de extensión con los parámetros a los que la aplicación de uso llama en su método
Startup.Configure . En el ejemplo siguiente, suponga que existe la siguiente firma del método de
comprobación de estado:

ExampleHealthCheck(string, string, int )

La firma anterior indica que la ExampleHealthCheck requiere datos adicionales para procesar la lógica de
sondeo de la comprobación de estado. Los datos se proporcionan al delegado que se usa para crear la
instancia de la comprobación de estado cuando la comprobación de estado se registra con un método de
extensión. En el ejemplo siguiente, el autor de llamada especifica los siguientes elementos opcionales:
nombre de la comprobación de estado ( name ). En el caso de null , se utiliza example_health_check .
punto de datos de cadena para la comprobación de estado ( data1 ).
punto de datos enteros para la comprobación de estado ( data2 ). En el caso de null , se utiliza 1 .
estado de error (HealthStatus). De manera predeterminada, es null . Si null , HealthStatus.Unhealthy
se notifica para un estado de error.
etiquetas ( IEnumerable<string> ).

using System.Collections.Generic;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public static class ExampleHealthCheckBuilderExtensions


{
const string NAME = "example_health_check";

public static IHealthChecksBuilder AddExampleHealthCheck(


this IHealthChecksBuilder builder,
string name = default,
string data1,
int data2 = 1,
HealthStatus? failureStatus = default,
IEnumerable<string> tags = default)
{
return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => new ExampleHealthCheck(data1, data2),
failureStatus,
tags));
}
}

Publicador de la comprobación de estado


Cuando un elemento IHealthCheckPublisher se agrega al contenedor de servicios, el sistema de comprobación
de estado ejecuta periódicamente las comprobaciones de estado y llama a PublishAsync con el resultado. Esto es
útil en un escenario de sistema de seguimiento de estado basado en inserción en el que se espera que cada
proceso llame periódicamente al sistema de seguimiento con el fin de determinar el estado.
La interfaz de IHealthCheckPublisher tiene un único método:

Task PublishAsync(HealthReport report, CancellationToken cancellationToken);

HealthCheckPublisherOptions le permiten establecer:


Delay – El retraso inicial aplicado tras iniciarse la aplicación antes de ejecutar instancias de
IHealthCheckPublisher. El retraso se aplica una vez durante el inicio y no se aplica a las iteraciones posteriores.
El valor predeterminado es cinco segundos.
Period – El período de ejecución de IHealthCheckPublisher. El valor predeterminado es 30 segundos.
Predicate – Si Predicate es null (valor predeterminado), el servicio de publicador de la comprobación de
estado ejecuta todas las comprobaciones de estado registradas. Para ejecutar un subconjunto de
comprobaciones de estado, proporcione una función que filtre el conjunto de comprobaciones. El predicado se
evalúa cada período.
Timeout – El tiempo de expiración para ejecutar las comprobaciones de estado para todas las instancias de
IHealthCheckPublisher. Use InfiniteTimeSpan para ejecutar sin tiempo de expiración. El valor predeterminado
es 30 segundos.

WARNING
En la versión de ASP.NET Core 2.2, el establecimiento de Period no se asigna por medio de la implementación de
IHealthCheckPublisher; establece el valor de Delay. Este problema se corregirá en ASP.NET Core 3.0. Para obtener más
información, consulte HealthCheckPublisherOptions.Period establece el valor de .Delay.
En la aplicación de ejemplo, ReadinessPublisher es una implementación de IHealthCheckPublisher. El estado de
comprobación de estado se registra en Entries y para cada comprobación:

public class ReadinessPublisher : IHealthCheckPublisher


{
private readonly ILogger _logger;

public ReadinessPublisher(ILogger<ReadinessPublisher> logger)


{
_logger = logger;
}

public List<(HealthReport report, CancellationToken cancellationToken)>


Entries { get; } =
new List<(HealthReport report,
CancellationToken cancellationToken)>();

public Exception Exception { get; set; }

public Task PublishAsync(HealthReport report,


CancellationToken cancellationToken)
{
Entries.Add((report, cancellationToken));

_logger.LogInformation("{TIMESTAMP} Readiness Probe Status: {RESULT}",


DateTime.UtcNow, report.Status);

if (Exception != null)
{
throw Exception;
}

cancellationToken.ThrowIfCancellationRequested();

return Task.CompletedTask;
}
}

En el ejemplo LivenessProbeStartup de la aplicación de ejemplo, la comprobación de preparación


StartupHostedService tiene un retraso de inicio de dos segundos y ejecuta la comprobación cada 30 segundos.
Para activar la implementación de IHealthCheckPublisher, el ejemplo registra ReadinessPublisher como servicio
singleton en el contenedor de inserción de dependencias (DI):
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
.AddCheck<StartupHostedServiceHealthCheck>(
"hosted_service_startup",
failureStatus: HealthStatus.Degraded,
tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(2);
options.Predicate = (check) => check.Tags.Contains("ready");
});

// The following workaround permits adding an IHealthCheckPublisher


// instance to the service container when one or more other hosted
// services have already been added to the app. This workaround
// won't be required with the release of ASP.NET Core 3.0. For more
// information, see: https://github.com/aspnet/Extensions/issues/639.
services.TryAddEnumerable(
ServiceDescriptor.Singleton(typeof(IHostedService),
typeof(HealthCheckPublisherOptions).Assembly
.GetType(HealthCheckServiceAssembly)));

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
}

NOTE
La siguiente solución alternativa permite la adición de una instancia de IHealthCheckPublisher al contenedor del servicio
cuando uno o más servicios hospedados ya se han agregado a la aplicación. Esta solución alternativa no se requerirá con el
lanzamiento de ASP.NET Core 3.0. Para obtener más información, consulte:
https://github.com/aspnet/Extensions/issues/639.

private const string HealthCheckServiceAssembly =


"Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckPublisherHostedService";

services.TryAddEnumerable(
ServiceDescriptor.Singleton(typeof(IHostedService),
typeof(HealthCheckPublisherOptions).Assembly
.GetType(HealthCheckServiceAssembly)));

NOTE
AspNetCore.Diagnostics.HealthChecks incluye editores para varios sistemas, como Application Insights.
AspNetCore.Diagnostics.HealthChecks es un puerto de BeatPulse y Microsoft no lo mantiene ni lo admite.
Hospedaje e implementación de ASP.NET Core
Blazor
19/06/2019 • 2 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Publicar la aplicación
Las aplicaciones se publican para implementación en la configuración de versión.
Visual Studio
Visual Studio Code y CLI de .NET Core
1. Seleccione Compilar > Publicar {aplicación} en la barra de navegación.
2. Seleccione el destino de publicación. Para publicar localmente, seleccione Carpeta.
3. Acepte la ubicación predeterminada del campo Elegir una carpeta o especifique una ubicación diferente.
Seleccione el botón Publicar.
Al publicar la aplicación se desencadena una restauración de las dependencias del proyecto y se compila el
proyecto antes de crear los recursos para la implementación. Como parte del proceso de compilación, se quitan
los ensamblados y métodos que no se usan para reducir los tiempos de carga y el tamaño de descarga de la
aplicación.
Una aplicación cliente de Blazor se publica en la carpeta /bin/Release/{RED DE DESTINO }/publish/{NOMBRE DE
ENSAMBLADO }/dist. Una aplicación de servidor de Blazor se publica en la carpeta /bin/Release/{TARGET
FRAMEWORK }/publish.
Los recursos de la carpeta se implementan en el servidor web. La implementación puede ser un proceso manual
o automatizado, en función de las herramientas de desarrollo que se usen.

Implementación
Para una guía sobre la implementación, consulte los temas siguientes:
Hospedaje e implementación de ASP.NET Core Blazor del lado cliente
Hospedaje e implementación de ASP.NET Core Blazor del lado servidor

Hospedaje sin servidor de Blazor con Azure Storage


Las aplicaciones de cliente de Blazor pueden obtenerse de Azure Storage como contenido estático directamente
desde un contenedor de almacenamiento.
Para más información, consulte Hospedaje e implementación de ASP.NET Core Blazor del lado cliente
(implementación independiente): Azure Storage.
Hospedaje e implementación de ASP.NET Core
Blazor del lado cliente
03/07/2019 • 24 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Valores de configuración de host


Las aplicaciones de Blazor que usan el modelo de hospedaje del lado cliente pueden aceptar los siguientes valores
de configuración de host como argumentos de línea de comandos en tiempo de ejecución en el entorno de
desarrollo.
Raíz del contenido
El argumento --contentroot establece la ruta de acceso absoluta al directorio que incluye los archivos de
contenido de la aplicación. En los ejemplos siguientes, /content-root-path es la ruta de acceso raíz del contenido
de la aplicación.
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --contentroot=/content-root-path

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--contentroot=/content-root-path"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--contentroot=/content-root-path

Ruta de acceso base


El argumento --pathbase establece la ruta de acceso base de la aplicación para una aplicación que se ejecuta
localmente con una ruta de acceso virtual que no es raíz (el valor href de la etiqueta <base> se establece en una
ruta de acceso que no sea / para ensayo y producción). En los ejemplos siguientes, /virtual-path es la ruta de
acceso base de la aplicación. Para obtener más información, vea la sección Ruta de acceso base de la aplicación.

IMPORTANT
A diferencia de la ruta de acceso proporcionada al valor href de la etiqueta <base> , no incluya una barra diagonal final (
/ ) al pasar el valor del argumento --pathbase . Si se proporciona la ruta de acceso base de la aplicación en la etiqueta
<base> como <base href="/CoolApp/"> (se incluye una barra diagonal final), se pasa el valor del argumento de línea de
comandos como --pathbase=/CoolApp (sin barra diagonal final).
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --pathbase=/virtual-path

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--pathbase=/virtual-path"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--pathbase=/virtual-path

Direcciones URL
El argumento --urls establece las direcciones IP o las direcciones de host con los puertos y protocolos en los
que escuchar las solicitudes.
Pase el argumento al ejecutar la aplicación de forma local en un símbolo del sistema. En el directorio de la
aplicación, ejecute lo siguiente:

dotnet run --urls=http://127.0.0.1:0

Agregue una entrada al archivo launchSettings.json de la aplicación en el perfil IIS Express. Esta
configuración se utiliza cuando se ejecuta la aplicación mediante el depurador de Visual Studio y desde un
símbolo del sistema con dotnet run .

"commandLineArgs": "--urls=http://127.0.0.1:0"

En Visual Studio, especifique el argumento en Propiedades > Depuración > Argumentos de la


aplicación. Al establecer el argumento en la página de propiedades de Visual Studio, se agrega el
argumento al archivo launchSettings.json.

--urls=http://127.0.0.1:0

Implementación
Con el modelo de hospedaje del lado cliente:
La aplicación Blazor, sus dependencias y el entorno de ejecución de .NET se descargan en el explorador.
La aplicación se ejecuta directamente en el subproceso de interfaz de usuario del explorador. Se puede seguir
cualquiera de las estrategias siguientes:
Una aplicación ASP.NET Core proporciona la aplicación Blazor. Esta estrategia se trata en la sección
Implementación hospedada con ASP.NET Core.
La aplicación Blazor se coloca en un servicio o servidor web de hospedaje estático, donde no se usa
.NET para proporcionar la aplicación Blazor. Esta estrategia se trata en la sección Implementación
independiente.

Configurar el enlazador
Blazor realiza la vinculación de lenguaje intermedio (IL ) en cada compilación para quitar el IL innecesario de los
ensamblados de salida. La vinculación de ensamblados puede controlarse en la compilación. Para más
información, consulte Configuración del enlazador para ASP.NET Core Blazor.

Reescritura de las URL para conseguir un enrutamiento correcto


Enrutar las solicitudes para los componentes de la página en una aplicación del lado cliente no es tan sencillo
como enrutar las solicitudes a una aplicación hospedada del lado servidor. Imagine que tiene una aplicación del
lado cliente con dos páginas:
_Main.razor: se carga en la raíz de la aplicación y contiene un vínculo a la página de información (
href="About" ).
_About.Razor: página Acerca de.
Cuando se solicita el documento predeterminado de la aplicación mediante la barra de direcciones del explorador
(por ejemplo, https://www.contoso.com/ ):
1. El explorador realiza una solicitud.
2. Se devuelve la página predeterminada, que suele ser index.html.
3. index.html arranca la aplicación.
4. Se carga el enrutador de Blazor y se muestra la página principal de Razor (Main.razor).
En la página principal, al seleccionar el vínculo a la página de información, se carga la página de información.
Seleccionar el vínculo a la página de información funciona en el cliente porque el enrutador de Blazor impide que
el explorador realice una solicitud en Internet a www.contoso.com sobre About y presenta la propia página de
información. Todas las solicitudes de páginas internas dentro de la aplicación del lado cliente funcionan del
mismo modo: Las solicitudes no desencadenan solicitudes basadas en el explorador a recursos hospedados en el
servidor en Internet. El enrutador controla las solicitudes de forma interna.
Si se realiza una solicitud mediante la barra de direcciones del explorador para www.contoso.com/About , se produce
un error. Este recurso no existe en el host de Internet de la aplicación, por lo que se devuelve una respuesta 404
No encontrado.
Dado que los exploradores realizan solicitudes a hosts basados en Internet para las páginas del lado cliente, los
servidores web y los servicios de hospedaje deben reescribir todas las solicitudes de recursos que no estén
físicamente en el servidor a la página index.html. Cuando se devuelve index.html, el enrutador de la aplicación del
lado cliente se hace cargo y responde con el recurso correcto.

Ruta de acceso base de la aplicación


La ruta de acceso base de la aplicación es la ruta de acceso raíz de la aplicación virtual en el servidor. Por ejemplo,
a una aplicación que reside en el servidor de Contoso, en una carpeta virtual de /CoolApp/ , se accede desde
https://www.contoso.com/CoolApp ; su ruta de acceso base virtual es /CoolApp/ . Al establecer la ruta de acceso base
de la aplicación en la ruta de acceso virtual ( <base href="/CoolApp/"> ), la aplicación sabe dónde reside
virtualmente en el servidor. La aplicación puede usar la ruta de acceso base de la aplicación para construir
direcciones URL relativas a la raíz de la aplicación desde un componente que no se encuentre en el directorio raíz.
Esto permite a los componentes que existen en diferentes niveles de la estructura de directorios compilar vínculos
a otros recursos en ubicaciones de toda la aplicación. La ruta de acceso base de la aplicación también se usa para
interceptar clics en hipervínculos en los que el destino href del vínculo está dentro del espacio de URI de la ruta
de acceso base de la aplicación y es el enrutador de Blazor quien controla la navegación interna.
En muchos escenarios de hospedaje, la ruta de acceso virtual del servidor a la aplicación es la raíz de la aplicación.
En estos casos, la ruta de acceso base de la aplicación es una barra diagonal ( <base href="/" /> ), que es la
configuración predeterminada para una aplicación. En otros escenarios de hospedaje, como las subaplicaciones o
los directorios virtuales de IIS y GitHub Pages, la ruta de acceso base de la aplicación debe establecerse en la ruta
de acceso virtual del servidor a la aplicación. Para establecer la ruta de acceso base de la aplicación, actualice la
etiqueta <base> dentro de los elementos de etiqueta <head> del archivo wwwroot/index.HTML. Establezca el
valor del atributo href en /virtual-path/ (la barra diagonal final es necesaria), donde /virtual-path/ es la ruta
de acceso raíz de la aplicación virtual completa en el servidor para la aplicación. En el ejemplo anterior, se
establece la ruta de acceso virtual en /CoolApp/ : <base href="/CoolApp/"> .
En el caso de una aplicación con una ruta de acceso virtual que no es raíz configurada (por ejemplo,
<base href="/CoolApp/"> ), la aplicación no puede encontrar sus recursos cuando se ejecuta de forma local. Para
solucionar este problema durante la fase de desarrollo y pruebas local, puede proporcionar un argumento de ruta
de acceso base que coincida con el valor href de la etiqueta <base> en tiempo de ejecución.
Para pasar el argumento de ruta de acceso base con la ruta de acceso raíz ( / ) al ejecutar la aplicación de forma
local, ejecute el comando dotnet run desde el directorio de la aplicación con la opción --pathbase :

dotnet run --pathbase=/{Virtual Path (no trailing slash)}

Para una aplicación con una ruta de acceso virtual base de /CoolApp/ ( <base href="/CoolApp/"> ), el comando es el
siguiente:

dotnet run --pathbase=/CoolApp

La aplicación responde de forma local en http://localhost:port/CoolApp .


Para más información, vea la sección sobre el valor de configuración de host de la ruta de acceso base.
Si una aplicación usa el modelo de hospedaje del lado cliente (basado en la plantilla de proyecto de Blazor; la
plantilla blazor al usar el comando dotnet new ) y se hospeda como una subaplicación de IIS en una aplicación
ASP.NET Core, es importante deshabilitar el controlador del módulo de ASP.NET Core heredado o asegurarse de
que la subaplicación no hereda la sección <handlers> de la aplicación raíz (principal) en el archivo web.config.
Para quitar el controlador del archivo web.config publicado de la aplicación, agregue una sección <handlers> al
archivo:

<handlers>
<remove name="aspNetCore" />
</handlers>

Como alternativa, deshabilite la herencia de la sección <system.webServer> de la aplicación raíz (principal)


mediante un elemento <location> con inheritInChildApplications establecido en false :
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" ... />
</handlers>
<aspNetCore ... />
</system.webServer>
</location>
</configuration>

Además de configurarse la ruta de acceso base de la aplicación, se quita el controlador o se deshabilita la herencia,
como se describe en esta sección. Establezca la ruta de acceso base de la aplicación en el archivo index.html de la
aplicación en el alias de IIS que ha usado al configurar la subaplicación en IIS.

Implementación hospedada con ASP.NET Core


Una implementación hospedada proporciona la aplicación Blazor del lado cliente a los exploradores desde una
aplicación ASP.NET Core que se ejecuta en un servidor.
La aplicación Blazor se incluye con la aplicación ASP.NET Core en la salida publicada para que ambas se
implementen juntas. Se requiere un servidor web que pueda hospedar una aplicación ASP.NET Core. En el caso
de una implementación hospedada, Visual Studio incluye la plantilla de proyecto de Blazor (hospedada en
ASP.NET Core) (la plantilla blazorhosted al usar el comando dotnet new ).
Para obtener más información sobre la implementación y el hospedaje de aplicaciones de ASP.NET Core, consulte
Hospedaje e implementación de ASP.NET Core.
Para obtener información sobre cómo implementar en Azure App Service, vea Publicar una aplicación de
ASP.NET Core en Azure con Visual Studio.

Implementación independiente
Una implementación independiente proporciona la aplicación Blazor del lado cliente como un conjunto de
archivos estáticos que los clientes solicitan directamente. Cualquier servidor de archivos estático es capaz de
servir a la aplicación Blazor.
Los activos de implementación independientes se publican en la carpeta /bin/Release/{RED DE
DESTINO }/publish/{NOMBRE DE ENSAMBLADO }/dist.
IIS
IIS es un servidor de archivos estáticos compatible con las aplicaciones de Blazor. Para configurar IIS para
hospedar Blazor, vea Build a Static Website on IIS (Compilación de un sitio web estático en IIS ).
Los recursos publicados se crean en la carpeta /bin/Release/{TARGET FRAMEWORK }/publish. Hospede el
contenido de la carpeta publish en el servidor web o el servicio de hospedaje.
web.config
Cuando se publica un proyecto de Blazor, se crea un archivo web.config con la siguiente configuración de IIS:
Se establecen los tipos MIME de las siguientes extensiones de archivo:
.dll – application/octet-stream
.json – application/json
.wasm – application/wasm
.woff – application/font-woff
.woff2 – application/font-woff
Se habilita la compresión HTTP de los siguientes tipos MIME:
application/octet-stream
application/wasm
Se establecen reglas del módulo URL Rewrite:
Proporcionar el subdirectorio donde residen los recursos estáticos de la aplicación ( {ASSEMBLY
NAME }/dist/{PATH REQUESTED } ).
Crear el enrutamiento de reserva de SPA para que las solicitudes de recursos que no sean archivos se
redirijan al documento predeterminado de la aplicación en su carpeta de recursos estáticos (
{ASSEMBLY NAME }/dist/index.html).
Instalación del módulo URL Rewrite
El módulo URL Rewrite es necesario para reescribir las URL. El módulo no se instala de forma predeterminada y
no está disponible para instalar como una característica de servicio de rol del servidor web (IIS ). El módulo se
debe descargar desde el sitio web de IIS. Use el instalador de plataforma web para instalar el módulo:
1. De forma local, vaya a la página de descargas del módulo URL Rewrite. En el caso de la versión en inglés,
seleccione WebPI para descargar el instalador de WebPI. En el caso de otros idiomas, seleccione la
arquitectura adecuada del servidor (x86/x64) para descargar el instalador.
2. Copie el instalador en el servidor. Ejecute el instalador. Haga clic en el botón Instalar y acepte los términos de
licencia. No es necesario reiniciar el servidor al finalizar la instalación.
Configuración del sitio web
Configure la ruta de acceso física del sitio web a la carpeta de la aplicación. La carpeta contiene:
El archivo web.config que usa IIS para configurar el sitio web, incluidas las reglas de redireccionamiento y los
tipos de contenido de archivos necesarios.
La carpeta de recursos estáticos de la aplicación.
Solución de problemas
Si se recibe un error 500 Error interno del servidor y el administrador de IIS produce errores al intentar acceder a
la configuración del sitio web, confirme que el módulo URL Rewrite está instalado. Si no lo está, IIS no puede
analizar el archivo web.config. Esto impide que el Administrador de IIS cargue la configuración del sitio web y que
el sitio web proporcione los archivos estáticos de Blazor.
Para obtener más información sobre cómo solucionar problemas de las implementaciones en IIS, vea Solución de
problemas de ASP.NET Core en IIS.
Almacenamiento de Azure
El hospedaje de archivos estáticos de Azure Storage permite el hospedaje de aplicaciones Blazor sin servidor. Se
admiten nombres de dominio personalizados, Azure Content Delivery Network (CDN ) y HTTPS.
Cuando el servicio de blob está habilitado para el hospedaje de sitios web estáticos en una cuenta de
almacenamiento:
Establece el nombre de documento de índice en index.html .
Establece la ruta de acceso del documento de error en index.html . Los componentes Razor y otros puntos
de conexión que no son de archivo no residen en las rutas de acceso físicas del contenido estático almacenado
por el servicio de blob. Cuando se recibe una solicitud de uno de estos recursos que debe controlar el
enrutador de Blazor, el error 404: no encontrado generado por el servicio de blob enruta la solicitud a la ruta
de acceso del documento de error. Se devuelve el blob index.html y el enrutador de Blazor carga y procesa
la ruta de acceso.
Para más información, consulte Hospedaje de sitios web estáticos en Azure Storage.
Nginx
El siguiente archivo nginx.conf se ha simplificado para mostrar cómo configurar Nginx para enviar el archivo
index.html siempre que no pueda encontrar un archivo correspondiente en el disco.

events { }
http {
server {
listen 80;

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
}
}
}

Para obtener más información sobre la configuración del servidor web de producción de Nginx, consulte Creating
NGINX Plus and NGINX Configuration Files (Creación de archivos de configuración de NGINX y NGINX Plus).
Nginx en Docker
Para hospedar Blazor en Docker mediante Nginx, configure el Dockerfile para usar la imagen de Nginx basada en
Alpine. Actualice el Dockerfile para copiar el archivo nginx.config en el contenedor.
Agregue una línea al Dockerfile, como se muestra en el ejemplo siguiente:

FROM nginx:alpine
COPY ./bin/Release/netstandard2.0/publish /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf

GitHub Pages
Para controlar las reescrituras de URL, agregue un archivo 404.html con un script que controle el
redireccionamiento de la solicitud a la página index.html. Para consultar una implementación de ejemplo que ha
proporcionado la comunidad, vea Single Page Apps for GitHub Pages (rafrex/spa-github-pages on GitHub)
(Aplicaciones de página única para GitHub Pages [rafrex/spa-github-pages en GitHub]). Se puede ver un ejemplo
del enfoque de la comunidad en blazor-demo/blazor-demo.github.io en GitHub (sitio activo).
Al usar un sitio de proyecto en lugar de un sitio de la organización, agregue o actualice la etiqueta <base> en
index.html. Defina el valor del atributo href con el nombre del repositorio de GitHub con una barra diagonal final
(por ejemplo, my-repository/ ).
Hospedaje e implementación de Blazor del lado
servidor
02/07/2019 • 2 minutes to read • Edit Online

Por Luke Latham, Rainer Stropek y Daniel Roth

Valores de configuración de host


Las aplicaciones del lado servidor que usan el modelo de hospedaje del lado servidor pueden aceptar valores de
configuración del host genérico.

Implementación
Con el modelo de hospedaje del lado servidor, Blazor se ejecuta en el servidor desde una aplicación ASP.NET
Core. Las actualizaciones de la interfaz de usuario, el control de eventos y las llamadas de JavaScript se controlan
mediante una conexión de SignalR.
Se requiere un servidor web que pueda hospedar una aplicación ASP.NET Core. Visual Studio incluye la plantilla
de proyecto Blazor (servidor) ( blazorserverside cuando se usa el comando dotnet new ).

Escalabilidad horizontal de la conexión


Las aplicaciones de servidor de Blazor requieren una conexión de SignalR activa para cada usuario. Una
implementación de servidor de Blazor de producción requiere una solución para admitir tantas conexiones
simultáneas como requiera la aplicación. Azure SignalR Service controla el escalado de conexiones y se
recomienda como solución de escalado para las aplicaciones de servidor de Blazor. Para más información,
consulte Publicar un ASP.NET Core SignalR app en Azure App Service.

Configuración de SignalR
ASP.NET Core configura SignalR para los escenarios de servidor de Blazor más habituales. Para escenarios
personalizados y avanzados, consulte los artículos de SignalR de la sección Recursos adicionales.

Recursos adicionales
Introducción a ASP.NET Core SignalR
Documentación de Azure SignalR Service
Inicio rápido: Creación de un salón de chat con SignalR Service
Hospedaje e implementación de ASP.NET Core
Publicar una aplicación de ASP.NET Core en Azure con Visual Studio
Implementar una versión preliminar de ASP.NET Core en Azure App Service
Configuración del enlazador para ASP.NET Core
Blazor
04/07/2019 • 2 minutes to read • Edit Online

Por Luke Latham


Blazor realiza la vinculación de lenguaje intermedio (IL ) durante una compilación de versión para quitar el IL
innecesario de los ensamblados de salida de la aplicación.
Controle la vinculación del ensamblado con cualquiera de los enfoques siguientes:
Deshabilitación de la vinculación global con una propiedad de MSBuild.
Control de la vinculación por cada ensamblado con un archivo de configuración.

Deshabilitación de la vinculación con una propiedad de MSBuild


La vinculación se habilita de forma predeterminada en el modo de versión cuando se crea una aplicación, lo que
incluye la publicación. Para deshabilitar la vinculación para todos los ensamblados, establezca la propiedad
BlazorLinkOnBuild de MSBuild en false en el archivo de proyecto:

<PropertyGroup>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
</PropertyGroup>

Control de la vinculación con un archivo de configuración


Control de la vinculación por cada ensamblado al proporcionar un archivo de configuración XML y especificar el
archivo como un elemento MSBuild en el archivo de proyecto:

<ItemGroup>
<BlazorLinkerDescriptor Include="Linker.xml" />
</ItemGroup>

Linker.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!--
This file specifies which parts of the BCL or Blazor packages must not be
stripped by the IL Linker even if they aren't referenced by user code.
-->
<linker>
<assembly fullname="mscorlib">
<!--
Preserve the methods in WasmRuntime because its methods are called by
JavaScript client-side code to implement timers.
Fixes: https://github.com/aspnet/Blazor/issues/239
-->
<type fullname="System.Threading.WasmRuntime" />
</assembly>
<assembly fullname="System.Core">
<!--
System.Linq.Expressions* is required by Json.NET and any
expression.Compile caller. The assembly isn't stripped.
-->
<type fullname="System.Linq.Expressions*" />
</assembly>
<!--
In this example, the app's entry point assembly is listed. The assembly
isn't stripped by the IL Linker.
-->
<assembly fullname="MyCoolBlazorApp" />
</linker>

Para obtener más información, consulte IL Linker: Syntax of xml descriptor (Vinculador de IL: sintaxis del
descriptor xml).
Introducción a la seguridad de ASP.NET Core
10/05/2019 • 3 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 HTTPS, 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, consulte otros artículos de la
sección Seguridad e identidad de la tabla de contenido.
Introducción a la identidad en ASP.NET Core
10/05/2019 • 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 (descarga)).
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.NET 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, 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. La
biblioteca de clases de identidad Razor expone los puntos de conexión con el Identity área. Por ejemplo:
/ Identidad o cuenta/inicio de sesión
/ Identidad o cuenta/cierre de sesión
/ Identidad o cuenta/administrar
Aplicación de migraciones
Aplicar las migraciones para inicializar la base de datos.
Visual Studio
CLI de .NET Core
Ejecute el siguiente comando en la consola de administrador de paquetes (PMC ):
PM> Update-Database

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} .
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>()
.AddDefaultUI(UIFramework.Bootstrap4)
.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_2);
}

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 para
generar el código se muestra en esta sección.
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: Consulte 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.
Un usuario intenta tener acceso a una página restringida que no están autorizados para tener acceso a o
cuando aún no se ha autenticado por el sistema.
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, consulta Introducción a la autorización en ASP.NET Core.
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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

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

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity"
asp-page="/Account/Manage/Index"
title="Manage">Hello@User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Page("/", new { area = "" })"
method="post">
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" 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 de privacidad.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApp1.Pages
{
[Authorize]
public class PrivacyModel : PageModel
{
public void OnGet()
{
}
}
}

Si ha iniciado sesión, cerrar sesión. Ejecute la aplicación y seleccione el privacidad 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 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
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
Autenticación y autorización para las spa
10/05/2019 • 21 minutes to read • Edit Online

ASP.NET Core 3.0 o posterior ofrece una autenticación en aplicaciones de una sola página (SPA) mediante la
compatibilidad para la autorización de API. ASP.NET Core Identity para autenticar y almacenar los usuarios se
combina con IdentityServer para la implementación de OpenID Connect.
Se agregó un parámetro de autenticación a la Angular y reaccionar plantillas que es similar al parámetro de la
autenticación de proyecto la aplicación Web (Model-View-Controller) (MVC ) y aplicación Web (las páginas
de Razor) plantillas de proyecto. Los valores de parámetros permitidos son ninguno y individuales. El React.js y
Redux plantilla de proyecto no es compatible con el parámetro de autenticación en este momento.

Crear una aplicación con el soporte técnico de autorización de API


Autorización y autenticación de usuario pueden utilizarse con Angular y React spa. Abra un shell de comandos y
ejecute el siguiente comando:
Angular:

dotnet new angular -o <output_directory_name> -au Individual

React:

dotnet new react -o <output_directory_name> -au Individual

El comando anterior crea una aplicación de ASP.NET Core con un ClientApp directorio que contiene la SPA.

Descripción general de los componentes de ASP.NET Core de la


aplicación
Las secciones siguientes describen las adiciones al proyecto cuando se incluye compatibilidad con la autenticación:
Clase de inicio
La Startup clase tiene las siguientes adiciones:
Dentro de la Startup.ConfigureServices método:
Identidad con la interfaz de usuario predeterminada:

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<ApplicationUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

IdentityServer con más AddApiAuthorization método auxiliar que configuraciones algunas


convenciones de ASP.NET Core en la parte superior IdentityServer de predeterminado:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

Autenticación con otros AddIdentityServerJwt método auxiliar que configura la aplicación para
validar los tokens JWT generado por IdentityServer:

services.AddAuthentication()
.AddIdentityServerJwt();

Dentro de la Startup.Configure método:


El middleware de autenticación que se encarga de validar las credenciales de la solicitud y establecer
el usuario en el contexto de solicitud:

app.UseAuthentication();

El middleware IdentityServer que expone los puntos de conexión de Open ID Connect:

app.UseIdentityServer();

AddApiAuthorization
Este método auxiliar configura IdentityServer para usar la configuración admitida. IdentityServer es un marco
eficaz y extensible para controlar cuestiones de seguridad de la aplicación. Al mismo tiempo, que expone una
complejidad innecesaria para los escenarios más comunes. Por lo tanto, se proporciona un conjunto de
convenciones y las opciones de configuración que se consideran un buen punto de partida. Una vez que la
autenticación de sus necesidades evolucionan, toda la eficacia de IdentityServer sigue estando disponible para
personalizar la autenticación para satisfacer sus necesidades.
AddIdentityServerJwt
Este método auxiliar configura una combinación de directivas para la aplicación como el controlador de
autenticación predeterminado. La directiva está configurada para permitir que la identidad de controlar todas las
solicitudes que se enrutan a cualquier subtrazado en el espacio de direcciones URL de identidad "/ identidad". El
JwtBearerHandler controla todas las demás solicitudes. Además, este método registra un <<ApplicationName>>API
recurso de API con IdentityServer con un ámbito predeterminado de <<ApplicationName>>API y configura el
middleware del token de portador de JWT para validar los tokens emitidos por IdentityServer para la aplicación.
SampleDataController
En el Controllers\SampleDataController.cs de archivo, tenga en cuenta el [Authorize] atributo aplicado a la clase
que indica que el usuario debe tener autorización en función de la directiva predeterminada para tener acceso al
recurso. La directiva de autorización predeterminado ocurre deberá estar configurado para usar el esquema de
autenticación predeterminado, que se configura de forma AddIdentityServerJwt en el esquema de la directiva que
se ha mencionado anteriormente, que hace el JwtBearerHandler configurado por el método de este tipo auxiliar
del controlador predeterminado para solicitudes a la aplicación.
ApplicationDbContext
En el Data\ApplicationDbContext.cs de archivos, haremos lo mismo DbContext identidad se usa con la excepción
que extiende ApiAuthorizationDbContext (más derivado de clase de IdentityDbContext ) para incluir el esquema
para IdentityServer.
Para tener control total sobre el esquema de base de datos, hereda de una de las identidades disponibles
DbContext clases y configurar el contexto para incluir el esquema de identidad mediante una llamada a
builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value) en el OnModelCreating método.
OidcConfigurationController
En el Controllers\OidcConfigurationController.cs de archivo, tenga en cuenta el punto de conexión que se
aprovisiona para dar servicio a los parámetros OIDC que el cliente debe usar.
appsettings.json
En el appsettings.json archivo de la raíz del proyecto, hay un nuevo IdentityServer configurado de sección que
describe la lista de clientes. En el ejemplo siguiente, hay un solo cliente. El nombre del cliente corresponde con el
nombre de la aplicación y se asigna por convención para el OAuth ClientId parámetro. El perfil indica el tipo de
aplicación que se está configurando. Se usa internamente para las convenciones de la unidad que simplifican el
proceso de configuración para el servidor. Hay varios perfiles disponibles, como se explica en el perfiles de
aplicación sección.

"IdentityServer": {
"Clients": {
"angularindividualpreview3final": {
"Profile": "IdentityServerSPA"
}
}
}

appsettings.Development.json
En el appsettings. Development.JSON archivo de la raíz del proyecto, hay un IdentityServer sección que describe
la clave utilizada para firmar los tokens. Al implementar en producción, debe una clave se aprovisione e
implemente junto con la aplicación, como se explica en el implementar en producción sección.

"IdentityServer": {
"Key": {
"Type": "Development"
}
}

Descripción general de la aplicación Angular


Admiten la autenticación y autorización de API en el Angular plantilla reside en su propio módulo Angular en el
ClientApp\src\api autorización directory. El módulo se compone de los siguientes elementos:
3 componentes:
login.component.ts: Controla el flujo de inicio de sesión de la aplicación.
logout.component.ts: Controla el flujo de cierre de sesión de la aplicación.
login-menu.component.ts: Un widget que muestra uno de los siguientes conjuntos de vínculos:
Administración de perfiles de usuario y vínculos cuando el usuario se autentica al cerrar la sesión.
Registro y registro en los vínculos cuando el usuario no está autenticado.
Una restricción de ruta AuthorizeGuard que pueden agregarse a las rutas y requiere que el usuario se
autentique antes de visitar la ruta.
Un interceptor HTTP AuthorizeInterceptor que asocia el token de acceso a las solicitudes HTTP salientes
destinadas a la API cuando se autentica el usuario.
Un servicio AuthorizeService que controla los detalles de bajo nivel del proceso de autenticación y expone
información sobre el usuario autenticado para el resto de la aplicación para su uso.
Un módulo Angular que define las rutas asociadas a las partes de la autenticación de la aplicación. Expone el
componente de menú de inicio de sesión, el interceptor, la protección y el servicio para el consumo del resto de
la aplicación.
Descripción general de la aplicación React
La compatibilidad para la autenticación y autorización de API en la plantilla de React reside en el
ClientApp\src\components\api autorización directory. Se compone de los siguientes elementos:
4 componentes:
Login.js: Controla el flujo de inicio de sesión de la aplicación.
Logout.js: Controla el flujo de cierre de sesión de la aplicación.
LoginMenu.js: Un widget que muestra uno de los siguientes conjuntos de vínculos:
Administración de perfiles de usuario y vínculos cuando el usuario se autentica al cerrar la sesión.
Registro y registro en los vínculos cuando el usuario no está autenticado.
AuthorizeRoute.js: Indica un componente de ruta que requiere que el usuario se autentique antes de
representar el componente en el Component parámetro.
Un exportado authService instancia de clase AuthorizeService que controla los detalles de bajo nivel del
proceso de autenticación y expone información sobre el usuario autenticado para el resto de la aplicación para
su uso.
Ahora que ha visto los componentes principales de la solución, se puede echar un vistazo más profundo en
distintos escenarios para la aplicación.

Requerir autorización sobre una nueva API


De forma predeterminada, el sistema está configurado para requerir autorización fácilmente para las nuevas API.
Para ello, cree un nuevo controlador y agregue el [Authorize] atributo a la clase de controlador o a cualquier
acción en el controlador.

Proteger una ruta del lado cliente (Angular)


Protección de una ruta del lado cliente se realiza mediante la adición de la protección de autorizar a la lista de
restricciones que se ejecutará cuando la configuración de una ruta. Por ejemplo, puede ver cómo el fetch-data
ruta está configurada en el módulo de aplicación principal Angular:

RouterModule.forRoot([
// ...
{ path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthorizeGuard] },
])

Es importante mencionar que la protección de una ruta no protege el punto de conexión real (que todavía se
requiere un [Authorize] atributo aplicado a él), pero que solo se evita que el usuario de navegación a la ruta del
lado cliente dada al que no está autenticado.

Autenticar solicitudes de API (Angular)


Autenticar solicitudes a las API hospedadas por la aplicación se realiza automáticamente mediante el uso del
interceptor de cliente HTTP definido por la aplicación.

Proteger una ruta del lado cliente (React)


Proteger una ruta del lado cliente mediante el AuthorizeRoute componente en lugar de con el valor plain Route
componente. Por ejemplo, observe cómo la fetch-data ruta está configurada en el App componente:
<AuthorizeRoute path='/fetch-data' component={FetchData} />

Protección de una ruta:


No protege el punto de conexión real (que todavía se requiere un [Authorize] atributo aplicado a él).
Solo impide al usuario navegar a la ruta del lado cliente dada al que no está autenticado.

Autenticar solicitudes de API (React)


Autenticar las solicitudes con React se realiza mediante la importación de la primera la authService instancia
desde el AuthorizeService . El token de acceso se recupera de la authService y se adjunta a la solicitud, tal como
se muestra a continuación. En los componentes de React, este trabajo se realiza normalmente en el
componentDidMount método del ciclo de vida o que el resultado de una interacción del usuario.

Importar el authService su componente

import authService from './api-authorization/AuthorizeService'

Recuperar y adjunte el token de acceso a la respuesta

async populateWeatherData() {
const token = await authService.getAccessToken();
const response = await fetch('api/SampleData/WeatherForecasts', {
headers: !token ? {} : { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
this.setState({ forecasts: data, loading: false });
}

Implementar en producción
Para implementar la aplicación en producción, deben aprovisionar los recursos siguientes:
Una base de datos para almacenar las cuentas de usuario de identidad y las concesiones IdentityServer.
Un certificado de producción que se usará para firmar los tokens.
No hay ningún requisito específico para este certificado; puede ser un certificado autofirmado o un
certificado a través de una entidad CA.
Se puede generar a través de herramientas estándares, como PowerShell o OpenSSL.
Puede ser instalado en el almacén de certificados en los equipos de destino o implementar como un .pfx
archivo con una contraseña segura.
Ejemplo: Implemente en Azure Websites
Esta sección describe la implementación de la aplicación en Azure websites que usan un certificado almacenado en
el almacén de certificados. Para modificar la aplicación para cargar un certificado del almacén de certificados, el
plan de App Service debe ser al menos el nivel estándar cuando se configura en un paso posterior. En la aplicación
appsettings.json de archivos, modifique la IdentityServer sección deben incluir los detalles claves:
"IdentityServer": {
"Key": {
"Type": "Store",
"StoreName": "My",
"StoreLocation": "CurrentUser",
"Name": "CN=MyApplication"
}
}

La propiedad nombre de certificado se corresponde con el distintivo asunto del certificado.


La ubicación del almacén representa dónde se debe cargar el certificado desde ( CurrentUser o LocalMachine ).
El nombre del almacén representa el nombre del almacén de certificados donde se almacena el certificado. En
este caso, señala al almacén personal del usuario.
Para implementar en Azure Websites, implemente la aplicación siguiendo los pasos de implementar la aplicación
en Azure para crear los recursos de Azure necesarios e implementar la aplicación en producción.
Después de seguir las instrucciones anteriores, la aplicación se implementa en Azure pero todavía no es funcional.
El certificado utilizado por la aplicación todavía debe configurarse. Busque la huella digital del certificado que se
usará y siga los pasos descritos en cargar sus certificados.
Aunque estos pasos mencionan SSL, no hay un certificados privados sección en el portal donde puede cargar el
certificado aprovisionado para usar con la aplicación.
Después de este paso, reinicie la aplicación y debe ser funcional.

Otras opciones de configuración


La compatibilidad para la autorización de API se basa en IdentityServer con un conjunto de convenciones, valores
predeterminados y mejoras para simplificar la experiencia para las spa. Obviamente, toda la eficacia de
IdentityServer está disponible en segundo plano si las aplicaciones integradas de ASP.NET Core no cubren su
escenario. La compatibilidad de ASP.NET Core se centra en las aplicaciones "propias", donde todas las
aplicaciones se crean e implementan en nuestra organización. Por lo tanto, no se ofrece soporte técnico para cosas
como el consentimiento o la federación. Para esos escenarios, use IdentityServer y seguir su documentación.
Perfiles de aplicación
Perfiles de aplicación son configuraciones predefinidas para las aplicaciones que definan sus parámetros. En este
momento, se admiten los siguientes perfiles:
IdentityServerSPA : Representa una SPA hospedada por IdentityServer como una sola unidad.
El redirect_uri el valor predeterminado es /authentication/login-callback .
El post_logout_redirect_uri el valor predeterminado es /authentication/logout-callback .
El conjunto de ámbitos incluye el openid , profile y cada ámbito definido para las API en la aplicación.
El conjunto de tipos permitidos de respuesta OIDC es id_token token o cada uno de ellos
individualmente ( id_token , token ).
El modo de respuesta permitidos es fragment .
SPA : Representa una SPA no está hospedada con IdentityServer.
El conjunto de ámbitos incluye el openid , profile y cada ámbito definido para las API en la aplicación.
El conjunto de tipos permitidos de respuesta OIDC es id_token token o cada uno de ellos
individualmente ( id_token , token ).
El modo de respuesta permitidos es fragment .
IdentityServerJwt : Representa una API que se hospeda junto con IdentityServer.
La aplicación está configurada para tener un ámbito que el valor predeterminado es el nombre de la
aplicación.
API : Representa una API que no está hospedada con IdentityServer.
La aplicación está configurada para tener un ámbito que el valor predeterminado es el nombre de la
aplicación.
Configuración a través de AppSettings
Configure las aplicaciones a través del sistema de configuración agregándolos a la lista de Clients o Resources .
Configuración de cada cliente redirect_uri y post_logout_redirect_uri propiedad, como se muestra en el
ejemplo siguiente:

"IdentityServer": {
"Clients": {
"MySPA": {
"Profile": "SPA",
"RedirectUri": "https://www.example.com/authentication/login-callback",
"LogoutUri": "https://www.example.com/authentication/logout-callback"
}
}
}

Al configurar los recursos, puede configurar los ámbitos para el recurso tal y como se muestra a continuación:

"IdentityServer": {
"Resources": {
"MyExternalApi": {
"Profile": "API",
"Scopes": "a b c"
}
}
}

Configuración mediante código


También puede configurar los clientes y los recursos a través de código mediante una sobrecarga de
AddApiAuthorization que realiza una acción para configurar las opciones.

AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>


{
options.Clients.AddSPA(
"My SPA", spa =>
spa.WithRedirectUri("http://www.example.com/authentication/login-callback")
.WithLogoutRedirectUri(
"http://www.example.com/authentication/logout-callback"));

options.ApiResources.AddApiResource("MyExternalApi", resource =>


resource.WithScopes("a", "b", "c"));
});

Recursos adicionales
Uso de la plantilla de proyecto de Angular con ASP.NET Core
Uso de la plantilla de proyecto de React con ASP.NET Core
Identidad de scaffold en proyectos de ASP.NET
Core
10/05/2019 • 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 para las páginas de Razor
~/Pages/Shared/_Layout.cshtml
~/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: Agregar el inicio de sesión parcial ( _LoginPartial ) para el archivo de diseño:

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

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">RazorNoAuth8</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - RazorNoAuth8</p>
</footer>
</div>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script 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 una existente _Layout.cshtml archivo seleccionado, 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: Agregar el inicio de sesión parcial ( _LoginPartial ) a la Views/Shared/_Layout.cshtml archivo:

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

<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">MvcNoAuth3</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>

<partial name="_CookieConsentPartial" />

<div class="container body-content">


@RenderBody()
<hr />
<footer>
<p>&copy; 2018 - MvcNoAuth3</p>
</footer>
</div>

<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
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 una existente _Layout.cshtml archivo seleccionado, 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
19/06/2019 • 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 PersonalDataAttribute 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.2 o posterior

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 2.2 de ASP.NET Core 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)}'.");
}

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

if (Input.Name != user.Name)
{
user.Name = Input.Name;
}

if (Input.DOB != user.DOB)
{
user.DOB = Input.DOB;
}

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";
ViewData["ActivePage"] = ManageNavPages.Index;
}

<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="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 id="update-profile-button" type="submit" class="btn btn-primary">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:

[AllowAnonymous]
public class RegisterModel : PageModel
{
private readonly SignInManager<WebApp1User> _signInManager;
private readonly UserManager<WebApp1User> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;

public RegisterModel(
UserManager<WebApp1User> userManager,
SignInManager<WebApp1User> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}

[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 void OnGet(string returnUrl = null)


{
ReturnUrl = returnUrl;
}

public async Task<IActionResult> OnPostAsync(string returnUrl = null)


{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new WebApp1User {
Name = Input.Name,
DOB = Input.DOB,
UserName = Input.Email,
Email = Input.Email
};
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
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";
}

<h1>@ViewData["Title"]</h1>

<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-primary">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.
Ejemplos de autenticación para ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

Por Rick Anderson


El repositorio de ASP.NET Core contiene los siguientes ejemplos de autenticación en el
AspNetCore/src/seguridad/samples carpeta:
Transformación de notificaciones
Autenticación con cookies
Proveedor de directivas personalizadas - IAuthorizationPolicyProvider
Opciones y los esquemas de autenticación dinámico
Notificaciones externas
Seleccionar entre la cookie y otro esquema de autenticación basada en la solicitud
Restringe el acceso a los archivos estáticos

Ejecutar los ejemplos


Seleccione un rama. Por ejemplo, release/2.2
Clone o descargue el repositorio de ASP.NET Core.
Compruebe que tiene instalada la SDK de .NET Core versión que coincida con la clonación del repositorio de
ASP.NET Core.
Navegue a un ejemplo en AspNetCore/src/seguridad/samples y ejecutar el ejemplo con dotnet run .
Personalización del modelo de identidad en ASP.NET
Core
02/07/2019 • 31 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. Las aplicaciones de producción normalmente generan scripts SQL a
partir de las migraciones e implementación cambios de base de datos como parte de una aplicación controlada y la
implementación de la 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)
{
}

protected override void OnModelCreating(ModelBuilder builder)


{
base.OnModelCreating(builder);
}
}

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 Pages/Shared/_LoginPartial.cshtml y reemplace IdentityUser con ApplicationUser :

@using Microsoft.AspNetCore.Identity
@using WebApp1.Areas.Identity.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

Actualización Areas/Identity/IdentityHostingStartup.cs o Startup.ConfigureServices y reemplace IdentityUser con


ApplicationUser .

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
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 OSS de la Comunidad
de ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

Esta página contiene las opciones de autenticación proporcionado por la Comunidad de código abierto para
ASP.NET Core. Esta página se actualiza periódicamente como nuevos proveedores disponibles.

Proveedores de autenticación de OSS


La siguiente lista se ordena alfabéticamente.

NAME DESCRIPCIÓN

AspNet.Security.OpenIdConnect.Server (ASOS) ASOS es un bajo nivel, first protocolo OpenID Connect marco
de servidor para ASP.NET Core y OWIN o Katana.

Cierge Cierge es un servidor de OpenID Connect que controla el


registro del usuario, inicio de sesión, perfiles, administración y
los inicios de sesión sociales.

Gluu Server Enterprise que esté listo, abra el software de código fuente
para la identidad, de acceso (IAM) de administración 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 OAuth 2.0 y OpenID Connect


para ASP.NET Core, oficialmente certificadas por la Fundación
OpenID y bajo control de .NET Foundation. Para obtener más
información, consulte Bienvenido a IdentityServer4
(documentación).

OpenIddict OpenIddict es un servidor de fácil de usar OpenID Connect


para ASP.NET Core.

Para agregar un proveedor, editar esta página.


Configurar la identidad de ASP.NET Core
10/05/2019 • 9 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.

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

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

PROPIEDAD 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

RequireLowercase Requiere un carácter en minúscula en la true


contraseña.
PROPIEDAD DESCRIPCIÓN DEFAULT

RequireNonAlphanumeric Requiere un carácter que no son true


alfanuméricos en la contraseña.

RequiredUniqueChars Solo se aplica a ASP.NET Core 2.0 o 1


posterior.

Requiere el número de caracteres


distintos de la contraseña.

RequireUppercase Requiere un carácter en mayúsculas en true


la contraseña.

PROPIEDAD 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

RequireLowercase Requiere un carácter en minúscula en la true


contraseña.

RequireNonAlphanumeric Requiere un carácter que no son true


alfanuméricos en la contraseña.

RequireUppercase Requiere un carácter en mayúsculas en true


la contraseña.

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.

PROPIEDAD DESCRIPCIÓN DEFAULT

RequireConfirmedEmail Requiere un correo electrónico false


confirmado al iniciar sesión.
PROPIEDAD DESCRIPCIÓN DEFAULT

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.

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

PasswordResetTokenProvider Obtiene o establece el IUserTwoFactorTokenProvider<TUser >


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.

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

Opciones de contraseña Hasher


PasswordHasherOptions Obtiene y establece las opciones para crear valores hash de contraseña.

OPCIÓN DESCRIPCIÓN
OPCIÓN DESCRIPCIÓN

CompatibilityMode El modo de compatibilidad que se usa al hash de contraseñas


nuevas. Tiene como valor predeterminado IdentityV3. El
primer byte de una contraseña con algoritmo hash,
denominado un marcador de formato, especifica la versión del
algoritmo hash utilizado para la contraseña. Al comprobar una
contraseña en un algoritmo hash, el VerifyHashedPassword
método selecciona el algoritmo correcto basándose en el
primer byte. Un cliente es puede autenticarse con
independencia de los cuales se usó la versión del algoritmo
para la contraseña. Establecer el modo de compatibilidad
afecta el hash de nuevas contraseñas.

IterationCount El número de iteraciones usadas al hash de contraseñas


mediante PBKDF2. Este valor es utiliza únicamente cuando el
CompatibilityMode está establecido en IdentityV3. El valor
debe ser un entero positivo y el valor predeterminado es
10000 .

En el ejemplo siguiente, la IterationCount está establecido en 12000 en Startup.ConfigureServices :

// using Microsoft.AspNetCore.Identity;

services.Configure<PasswordHasherOptions>(option =>
{
option.IterationCount = 12000;
});
Configurar la autenticación de Windows en
ASP.NET Core
02/07/2019 • 19 minutes to read • Edit Online

Por Scott Addie y Luke Latham


Se puede configurar la autenticación de Windows (también conocida como autenticación Negotiate,
Kerberos o NTLM ) para las aplicaciones de ASP.NET Core hospedadas con IIS, Kestrel, o HTTP.sys .
Se puede configurar la autenticación de Windows (también conocida como autenticación Negotiate,
Kerberos o NTLM ) para las aplicaciones de ASP.NET Core hospedadas con IIS o HTTP.sys.
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 cuentas de Windows para identificar a
los usuarios. Autenticación de Windows se adapta mejor a los entornos de intranet donde los usuarios, las
aplicaciones cliente y servidores web pertenecen al mismo dominio de Windows.

NOTE
No se admite la autenticación de Windows con HTTP/2. Desafíos de autenticación se pueden enviar en las respuestas
HTTP/2, pero el cliente debe cambiar a HTTP/1.1 antes de autenticar.

IIS/IIS Express
Agregar servicios de autenticación mediante la invocación AddAuthentication
(Microsoft.AspNetCore.Server.IISIntegration espacio de nombres) en Startup.ConfigureServices :

services.AddAuthentication(IISDefaults.AuthenticationScheme);

Iniciar configuración (depurador)


Solo afecta la configuración para la configuración de inicio la Properties/launchSettings.json de archivos
para IIS Express y no configura IIS para la autenticación de Windows. Configuración del servidor se explica
en el IIS sección.
El aplicación Web plantilla disponible a través de Visual Studio o la CLI de .NET Core puede configurarse
para admitir la autenticación de Windows, que actualiza el Properties/launchSettings.json archivo de forma
automática.
Visual Studio
Visual Studio Code y CLI de .NET Core
nuevo proyecto
1. Cree un nuevo proyecto.
2. Seleccione Aplicación web de ASP.NET Core. Seleccione Siguiente.
3. Proporcione un nombre en el nombre del proyecto campo. Confirme la ubicación entrada es correcta
o proporcionar una ubicación para el proyecto. Seleccione Crear.
4. Seleccione cambio en autenticación.
5. En el Cambiar autenticación ventana, seleccione Windows autenticación. Seleccione Aceptar.
6. Seleccione Aplicación web.
7. Seleccione Crear.
Ejecutar la aplicación. El nombre de usuario aparece en la interfaz de usuario de la aplicación representada.
Proyecto existente
Las propiedades del proyecto habilitar la autenticación de Windows y deshabilitar la autenticación anónima:
1. Haga clic con el botón derecho en el proyecto en el Explorador de soluciones y seleccione
Propiedades.
2. Seleccione la pestaña Depurar.
3. Desactive la casilla de verificación habilitar la autenticación anónima.
4. Seleccione la casilla de verificación habilitar la autenticación de Windows.
5. Guarde y cierre la página de propiedades.
Como alternativa, se pueden configurar las propiedades en el iisSettings nodo de la launchSettings.json
archivo:

"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:52171/",
"sslPort": 44308
}
}

Cuando se modifica un proyecto existente, compruebe que el archivo de proyecto incluye una referencia de
paquete para el Microsoft.AspNetCore.App metapaquete o el Microsoft.AspNetCore.Authentication
paquete NuGet.
IIS
IIS usa el módulo ASP.NET Core para hospedar aplicaciones ASP.NET Core. Autenticación de Windows se
configura para IIS a través de la web.config archivo. Las siguientes secciones muestran cómo:
Proporcionar una variable local web.config archivo que activa la autenticación de Windows en el servidor
cuando se implementa la aplicación.
Use el Administrador de IIS para configurar el web.config archivos de una aplicación de ASP.NET Core
que ya se ha implementado en el servidor.
Si aún no lo ha hecho, habilite IIS para hospedar aplicaciones ASP.NET Core. Para obtener más
información, consulta Hospedaje de ASP.NET Core en Windows con 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 IIS Integration 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: Atributos del elemento aspNetCore.
Use cualquier de los métodos siguientes:
Antes de publicar e implementar el proyecto, agregue las siguientes web.config archivo a la raíz
del proyecto:

<?xml version="1.0" encoding="utf-8"?>


<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false" />
<windowsAuthentication enabled="true" />
</authentication>
</security>
</system.webServer>
</location>
</configuration>

Cuando se publica el proyecto mediante el SDK de .NET Core (sin el <IsTransformWebConfigDisabled>


propiedad establecida en true en el archivo de proyecto), publicado web.config archivo incluye la
<location><system.webServer><security><authentication> sección. Para obtener más información
sobre la <IsTransformWebConfigDisabled> propiedad, vea Hospedaje de ASP.NET Core en Windows
con IIS.
Después de publicar e implementar el proyecto, realizar la configuración de servidor con el
Administrador de IIS:
1. En el Administrador de IIS, seleccione el sitio IIS en el sitios nodo de la conexiones barra lateral.
2. Haga doble clic en autenticación en el IIS área.
3. Seleccione autenticación anónima. Seleccione deshabilitar en el acciones barra lateral.
4. Seleccione Windows autenticación. Seleccione habilitar en el acciones barra lateral.
Cuando se realizan estas acciones, el Administrador de IIS modifica la aplicación web.config archivo.
Un <system.webServer><security><authentication> se agrega el nodo con la configuración actualizada
para anonymousAuthentication y windowsAuthentication :

<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false" />
<windowsAuthentication enabled="true" />
</authentication>
</security>
</system.webServer>

El <system.webServer> sección agregada a la web.config archivo por el Administrador de IIS está


fuera de la aplicación <location> sección agregada por el SDK de .NET Core cuando se publique la
aplicación. Dado que se agrega la sección fuera de la <location> nodo, la configuración se hereda
por cualquier las aplicaciones secundarias a la aplicación actual. Para impedir la herencia, mover
agregado <security> sección dentro de la <location><system.webServer> sección suministradas por
el SDK de .NET Core.
Cuando se usa el Administrador de IIS para agregar la configuración de IIS, solo afecta a la aplicación
web.config archivo en el servidor. Una implementación posterior de la aplicación podría sobrescribir
la configuración en el servidor si la copia del servidor de web.config se sustituye por el proyecto
web.config archivo. Use cualquier de los métodos siguientes para administrar la configuración:
Use el Administrador de IIS para restablecer la configuración en el web.config archivo después de
que el archivo se sobrescribe en la implementación.
Agregar un archivo web.config a la aplicación localmente con la configuración.

Kestrel
El Microsoft.AspNetCore.Authentication.Negotiate se puede usar el paquete NuGet con Kestrel para admitir
la autenticación de Windows mediante Negotiate, Kerberos y NTLM en Windows, Linux y macOS.

WARNING
Las credenciales pueden conservarse entre las solicitudes en una conexión. Negociar la autenticación no debe usarse
con servidores proxy a menos que el proxy mantiene una afinidad de conexión de 1:1 (una conexión persistente)
con Kestrel.

NOTE
El controlador Negotiate detecta si el servidor subyacente admite autenticación de Windows de forma nativa y si está
habilitado. Si el servidor admite la autenticación de Windows, pero está deshabilitado, se produce un error que le pide
que habilite la implementación del servidor. Cuando se habilita la autenticación de Windows en el servidor, el
controlador Negotiate reenvía de manera transparente a él.

Agregar servicios de autenticación mediante la invocación AddAuthentication (


Microsoft.AspNetCore.Authentication.Negotiate espacio de nombres) y AddNegotitate (
Microsoft.AspNetCore.Authentication.Negotiate espacio de nombres) en Startup.ConfigureServices :

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();

Agregue el Middleware de autenticación mediante una llamada a UseAuthentication en Startup.Configure :

app.UseAuthentication();

app.UseMvc();

Para obtener más información sobre el software intermedio, consulte Middleware de ASP.NET Core.
Se permiten las solicitudes anónimas. Use autorización de ASP.NET Core a las solicitudes anónimas para la
autenticación de desafío.
Configuración del entorno de Windows
El Microsoft.AspNetCore.Authentication.Negotiate componente realiza la autenticación de modo de usuario.
Nombres de entidad de servicio (SPN ) debe agregarse a la cuenta de usuario que ejecuta el servicio, no la
cuenta de equipo. Ejecutar setspn -S HTTP/mysrevername.mydomain.com myuser en un shell de comandos
administrativas.
Configuración del entorno de Linux y macOS
Las instrucciones para unir una máquina Linux o macOS a un dominio de Windows están disponibles en el
conectar Studio datos de Azure a SQL Server mediante la autenticación de Windows - Kerberos artículo. Las
instrucciones de creación una cuenta de equipo para la máquina de Linux en el dominio. Los SPN deben
agregarse a esa cuenta de equipo.
NOTE
Al seguir las instrucciones de la conectar Studio datos de Azure a SQL Server mediante la autenticación de Windows -
Kerberos artículo, reemplace python-software-properties con python3-software-properties si es necesario.

Una vez que la máquina Linux o macOS está unida al dominio, se requieren pasos adicionales para
proporcionar un archivo keytab con los SPN:
En el controlador de dominio, agregue SPN del servicio web nuevo a la cuenta de equipo:
setspn -S HTTP/mywebservice.mydomain.com mymachine
setspn -S HTTP/mywebservice@MYDOMAIN.COM mymachine
Use ktpass para generar un archivo keytab:
ktpass -princ HTTP/mywebservice.mydomain.com@MYDOMAIN.COM -pass myKeyTabFilePassword -mapuser
MYDOMAIN\mymachine$ -pType KRB5_NT_PRINCIPAL -out c:\temp\mymachine.HTTP.keytab -crypto AES256-
SHA1
Algunos campos deben especificarse en mayúsculas como se indica.
Copie el archivo keytab en el equipo Linux o macOS.
Seleccione el archivo keytab a través de una variable de entorno:
export KRB5_KTNAME=/tmp/mymachine.HTTP.keytab
Invocar klist para mostrar los SPN disponibles actualmente para su uso.

NOTE
Un archivo keytab contiene las credenciales de acceso de dominio y debe protegerse en consecuencia.

HTTP.sys
HTTP.sys admite la autenticación de Windows de modo Kernel con Negotiate, NTLM o autenticación básica.
Agregar servicios de autenticación mediante la invocación AddAuthentication
(Microsoft.AspNetCore.Server.HttpSys espacio de nombres) en Startup.ConfigureServices :

services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);

Configurar el host de la aplicación web para usar HTTP.sys con autenticación de Windows (Program.cs).
UseHttpSys en el Microsoft.AspNetCore.Server.HttpSys espacio de nombres.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>


Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.Authentication.Schemes =
AuthenticationSchemes.NTLM |
AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
});
});
}

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.

NOTE
HTTP.sys no se admite en Nano Server versión 1709 o posterior. Para usar autenticación de Windows y HTTP.sys con
Nano Server, use un contenedor de Server Core (microsoft/windowsservercore). Para obtener más información sobre
Server Core, vea ¿qué es la opción de instalación Server Core en Windows Server?.

Autorizar a los usuarios


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 un sitio de IIS 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 los puntos de conexión de la
aplicación que requieren autenticación. El [AllowAnonymous] atributo invalida la [Authorize] atributo en las
aplicaciones que permiten el acceso anónimo. Para obtener detalles de uso de atributo, vea Autorización
simple en ASP.NET Core.

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

Suplantación
ASP.NET Core no implementa la suplantación. Las aplicaciones se ejecutan con la identidad de la aplicación
para todas las solicitudes, utilizando la identidad de proceso o grupo de servidores de aplicación. Si la
aplicación debe realizar una acción en nombre de un usuario, use WindowsIdentity.RunImpersonated en un
software intermedio alineado terminal en Startup.Configure . 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}\t" +
$"State: {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());
}
});

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.
Mientras el Microsoft.AspNetCore.Authentication.Negotiate paquete habilita la autenticación de Windows,
Linux y macOS, suplantación solo se admite en Windows.

Transformaciones de notificaciones
Al hospedarse con IIS, AuthenticateAsync no se llama internamente para inicializar un usuario. Por tanto, se
usa una implementación de IClaimsTransformation para transformar las notificaciones después de que cada
autenticación no se active de forma predeterminada. Para obtener más información y un ejemplo de código
que activa las transformaciones de notificaciones, consulte Módulo ASP.NET Core.
Al hospedar con el modo en proceso IIS, AuthenticateAsync no se llama internamente para inicializar un
usuario. Por tanto, se usa una implementación de IClaimsTransformation para transformar las notificaciones
después de que cada autenticación no se active de forma predeterminada. Para obtener más información y
un ejemplo de código que activa las transformaciones de notificaciones cuando se hospedan en proceso,
consulte Módulo ASP.NET Core.

Recursos adicionales
dotnet publish
Hospedaje de ASP.NET Core en Windows con IIS
Módulo ASP.NET Core
Perfiles de publicación de Visual Studio para la implementación de aplicaciones ASP.NET Core
Proveedores de almacenamiento personalizados para
ASP.NET Core Identity
10/05/2019 • 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. Los almacenes siguen el patrón de repositorio y se acoplan
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
Role Storage
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
PROPINA: 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
IUserPhoneNumberStore
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
20/05/2019 • 8 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.2 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.

El hecho de permitir a los usuarios iniciar sesión con sus credenciales:


resulta muy práctico para ellos;
transfiere muchas de las complejidades de administrar el 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.

Crear un proyecto de ASP.NET Core


Visual Studio
Visual Studio Code
Visual Studio para Mac
Cree un nuevo proyecto.
Seleccione Aplicación web de ASP.NET Core y Siguiente.
Proporcione un Nombre del proyecto y confirme o cambie la Ubicación. Seleccione Crear.
Seleccione ASP.NET Core 2.2 en la lista desplegable. Seleccione Aplicación web en la lista de plantillas.
En Autenticación, seleccione Cambiar y establezca la autenticación en Cuentas de usuario
individuales. Seleccione Aceptar.
En la ventana Crear una aplicación web ASP.NET Core, seleccione Crear.

Aplicación de migraciones
Ejecute la aplicación y seleccione el vínculo Registrar.
Escriba el correo electrónico y la contraseña de la cuenta nueva y, luego, seleccione Registrarse.
Siga estas instrucciones para aplicar las migraciones.

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la
información de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud.
Normalmente, esta información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del
cliente. Las aplicaciones no leen automáticamente estos encabezados de solicitud para detectar y usar la
información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos.
El resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas
poco seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para
la aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.

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

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 => { ... });

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:
Seleccione el vínculo Hola, <alias de correo electrónico> situado en la esquina superior derecha para ir
a la vista Administración.

Seleccione 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 Google
en ASP.NET Core
24/06/2019 • 8 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Google + API heredadas que se haya cerrado a partir del 7 de marzo de 2019. Google + iniciar sesión y los
desarrolladores deben mover a un nuevo inicio de sesión de Google en el sistema. Los paquetes ASP.NET Core
2.1 y 2.2 para la autenticación de Google se ha actualizado para adaptarse a los cambios. Para obtener más
información y mitigaciones temporales para ASP.NET Core, consulte este problema de GitHub. En este tutorial
se ha actualizado con el nuevo proceso de instalación.
Este tutorial muestra cómo habilitar usuarios iniciar sesión con su cuenta de Google con el proyecto de ASP.NET
Core 2.2 creado en el página anterior.

Crear un identificador de cliente y el proyecto de consola de API de


Google
Vaya a la integración de Google signo In de la aplicación web y seleccione configurar un proyecto.
En el configuración del cliente de OAuth cuadro de diálogo, seleccione servidor Web.
En el URI de redireccionamiento autorizado cuadro de entrada de texto, establecer el URI de
redireccionamiento. Por ejemplo, https://localhost:5001/signin-google .
Guardar el Id. de cliente y secreto de cliente.
Al implementar el sitio, registrar la nueva url pública desde el Google Console.

Store Google ClientID y ClientSecret


Store valores confidenciales, como Google Client ID y Client Secret con el Secret Manager. Para los fines de
este tutorial, asigne el nombre de los tokens Authentication:Google:ClientId y
Authentication:Google:ClientSecret :

dotnet user-secrets set "Authentication:Google:ClientId" "X.apps.googleusercontent.com"


dotnet user-secrets set "Authentication:Google:ClientSecret" "<client secret>"

Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos ( : ) no
funcione en todas las plataformas (por ejemplo, Bash). Un guion bajo doble ( __ ) es compatible con todas las
plataformas y lo reemplaza un separador de dos puntos.
Puede administrar las credenciales de la API y el uso en el consola de API.

Configurar la autenticación de Google


Agregar el servicio de Google Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication()
.AddGoogle(options =>
{
IConfigurationSection googleAuthNSection =
Configuration.GetSection("Authentication:Google");

options.ClientId = googleAuthNSection["ClientId"];
options.ClientSecret = googleAuthNSection["ClientSecret"];
});

services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

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 Google


Ejecute la aplicación y haga clic en inicie sesión. Aparece una opción para iniciar sesión con Google.
Haga clic en el Google botón, que redirige a Google para la autenticación.
Después de escribir sus credenciales de Google, se le redirigirá al sitio web.

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información
de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta
información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones
no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.
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 => { ... });

Consulte la GoogleOptions referencia de API para obtener más información sobre las opciones de configuración
compatible con la autenticación de Google. Esto puede utilizarse para solicitar información diferente sobre el
usuario.

Cambiar el URI de devolución de llamada predeterminada


El segmento del 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.

Solución de problemas
Si el inicio de sesión no funciona y no recibe algún error, cambie al modo de desarrollo para hacer más fácil
de depurar el problema.
Si la identidad no está configurada mediante una llamada a services.AddIdentity en ConfigureServices , al
intentar autenticar los resultados en 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. Seleccione aplicar migraciones para crear la base de datos y
actualice la página para continuar más allá del error.

Pasos siguientes
Este artículo, mostramos 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 publique la aplicación en Azure, restablezca el 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 Azure portal. El sistema de configuración está configurado para leer las claves de las variables
de entorno.
Configuración de inicio de sesión externo de
Facebook en ASP.NET Core
10/05/2019 • 10 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este tutorial con ejemplos de código 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. 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 .

Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos ( : ) no
funcione en todas las plataformas (por ejemplo, Bash). Un guion bajo doble ( __ ) es compatible con todas las
plataformas y lo reemplaza un separador de dos puntos.
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


Agregue el servicio de Facebook en la ConfigureServices método en el Startup.cs archivo:

services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
});

La llamada a AddDefaultIdentity 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.

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 => { ... });

Instalar el Microsoft.AspNetCore.Authentication.Facebook paquete.


Para instalar este paquete con Visual Studio 2017, haga doble clic en el proyecto y seleccione administrar
paquetes de NuGet.
Para instalar con la CLI de .NET Core, ejecute lo siguiente en el directorio del proyecto:
dotnet add package Microsoft.AspNetCore.Authentication.Facebook
Agregue el middleware de Facebook en la Configure método Startup.cs archivo:

app.UseFacebookAuthentication(new FacebookOptions()
{
AppId = Configuration["Authentication:Facebook:AppId"],
AppSecret = Configuration["Authentication:Facebook:AppSecret"]
});

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:

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información
de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta
información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones
no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.

Solución de problemas
ASP.NET Core 2.x solo: Si la identidad no está configurada 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
Agregar el Microsoft.AspNetCore.Authentication.Facebook paquete NuGet al proyecto para escenarios
avanzados de autenticación de Facebook. Este paquete no es necesario integrar la funcionalidad de inicio
de sesión externo de Facebook con su aplicación.
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.
Configuración de inicio de sesión externo Account
de Microsoft con ASP.NET Core
24/06/2019 • 9 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este ejemplo muestra cómo habilitar usuarios iniciar sesión con su cuenta de Microsoft con el proyecto de
ASP.NET Core 2.2 creado en el página anterior.

Crear la aplicación en el Portal para desarrolladores de Microsoft


Navegue hasta la Azure portal: registros de aplicaciones página y crear o iniciar sesión en una cuenta de
Microsoft:
Si no tienes una cuenta de Microsoft, seleccione crearla. Después de iniciar sesión se le redirigirá a la registros
de aplicaciones página:
Seleccione nuevo registro
Escriba un nombre.
Seleccione una opción para admite tipos de cuenta.
En URI de redireccionamiento, escriba la dirección URL de desarrollo con /signin-microsoft anexado. Por
ejemplo: https://localhost:44389/signin-microsoft . El esquema de autenticación de Microsoft configurado
más adelante en este ejemplo controlará automáticamente las solicitudes en /signin-microsoft ruta para
implementar el flujo de OAuth.
Seleccione registrar
Crear el secreto de cliente
En el panel izquierdo, seleccione certificados y secretos.
En los secretos de cliente, seleccione nuevo secreto de cliente
Agregue una descripción para el secreto de cliente.
Seleccione el agregar botón.
En los secretos de cliente, copie el valor del secreto de cliente.

NOTE
El segmento del 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.

El secreto de cliente y el identificador de cliente de Microsoft Store


Ejecute los comandos siguientes para almacenar de forma segura ClientId y ClientSecret mediante Secret
Manager:
dotnet user-secrets set Authentication:Microsoft:ClientId <Client-Id>
dotnet user-secrets set Authentication:Microsoft:ClientSecret <Client-Secret>

Vincular configuración confidencial, como Microsoft ClientId y ClientSecret para la configuración de


aplicación mediante el Secret Manager. Para los fines de este ejemplo, asigne el nombre de los tokens
Authentication:Microsoft:ClientId y Authentication:Microsoft:ClientSecret .

Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos ( : ) no
funcione en todas las plataformas (por ejemplo, Bash). Un guion bajo doble ( __ ) es compatible con todas las
plataformas y lo reemplaza un separador de dos puntos.

Configurar la autenticación de la cuenta de Microsoft


Agregar el servicio de Microsoft Account Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

La llamada a AddDefaultIdentity 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.

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 => { ... });

Consulte la MicrosoftAccountOptions referencia de API para obtener más información sobre las opciones de
configuración compatible con la autenticación de Microsoft Account. Esto puede utilizarse para solicitar
información diferente sobre el usuario.

Inicie sesión con la cuenta de Microsoft


Ejecute el y haga clic en inicie sesión. Aparece una opción para iniciar sesión en 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 aún no ha iniciado sesión) se le indicará que permiten que la aplicación acceso a tu información:
Pulse Sí y se le redirigirá al sitio web donde puede establecer su correo electrónico.
Ha iniciado sesión con sus credenciales de Microsoft:

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información
de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta
información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones
no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.

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 cadena parámetros de consulta justo después de la # (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 del Uri no coincide con cualquiera de los URI de redirección especificado
para el Web plataforma .
Si la identidad no está configurada mediante una llamada a services.AddIdentity en ConfigureServices ,
intentando autenticarse producirá ArgumentException: Se debe proporcionar la opción 'SignInScheme' .
La plantilla de proyecto utilizada en este ejemplo 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 Microsoft. 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, cree a un nuevo cliente secretos en el
Portal para desarrolladores de Microsoft.
Establecer el Authentication:Microsoft:ClientId y Authentication:Microsoft:ClientSecret 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.
Twitter inicio de sesión de instalación externo con
ASP.NET Core
10/05/2019 • 7 minutes to read • Edit Online

Por Valeriy Novytskyy y Rick Anderson


Este ejemplo muestra cómo habilitar usuarios para inicie sesión con su cuenta de Twitter usando un proyecto de
ASP.NET Core 2.2 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 no dispone de una cuenta de Twitter, use el Suscríbase
ahora vínculo para crear uno.
Pulse crear nueva aplicación 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 anexado a la URI de redirección de OAuth válido
campo (por ejemplo: https://webapp128.azurewebsites.net/signin-twitter ). El esquema de autenticación de
Twitter configurado más adelante en este ejemplo controlará automáticamente las solicitudes en
/signin-twitter ruta para implementar el flujo de OAuth.

NOTE
El segmento del 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 aplicación de Twitter. Se muestran los detalles de la nueva
aplicación:

Almacenar la clave de API de consumidor de Twitter y el secreto


Ejecute los comandos siguientes para almacenar de forma segura ClientId y ClientSecret mediante Secret
Manager:

dotnet user-secrets set Authentication:Twitter:ConsumerAPIKey <Key>


dotnet user-secrets set Authentication:Twitter:ConsumerAPISecret <Secret>

Vincular los valores confidenciales como Twitter Consumer Key y Consumer Secret para la configuración de
aplicación mediante el Secret Manager. Para los fines de este ejemplo, asigne el nombre de los tokens
Authentication:Twitter:ConsumerKey y Authentication:Twitter:ConsumerSecret .

Estos tokens pueden encontrarse en el claves y Tokens de acceso ficha después de crear una nueva aplicación
de Twitter:

Configurar la autenticación de Twitter


Agregue el servicio de Twitter en el ConfigureServices método Startup.cs archivo:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication().AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Authentication:Twitter:ConsumerAPIKey"];
twitterOptions.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

La llamada a AddDefaultIdentity 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.

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 => { ... });

Consulte la TwitterOptions referencia de API para obtener más información sobre las opciones de configuración
compatible con la autenticación de Twitter. Esto puede utilizarse para solicitar información diferente sobre el
usuario.

Inicie sesión con Twitter


Ejecute la aplicación y seleccione inicie 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 su correo
electrónico.
Ha iniciado sesión con sus credenciales de Twitter:

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información
de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta
información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones
no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.

Solución de problemas
ASP.NET Core 2.x solo: Si la identidad no está configurada mediante una llamada a services.AddIdentity
en ConfigureServices , intentando autenticarse producirá ArgumentException: Se debe proporcionar la opción
'SignInScheme'. La plantilla de proyecto utilizada en este ejemplo 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 Twitter. 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 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 Azure portal. El sistema de configuración está configurado para leer las
claves de las variables de entorno.
Proveedores de autenticación de OAuth externos
10/05/2019 • 2 minutes to read • Edit Online

Por Rick Anderson, Pranav Rastogi, y Valeriy Novytskyy


En la lista siguiente incluye comunes proveedores externos de autenticación de OAuth que funcionan con
aplicaciones ASP.NET Core. Paquetes de NuGet de terceros, como los que se mantiene aspnet-contrib, puede
usarse para complementar los proveedores de autenticación implementados por el equipo de ASP.NET Core.
LinkedIn (Instructions)
Instagram (instrucciones)
Reddit (instrucciones)
Github (instrucciones)
Yahoo (instrucciones)
Tumblr (instrucciones)
Pinterest (instrucciones)
Pocket (instrucciones)
Flickr (instrucciones)
Dribble (instrucciones)
Vimeo (instrucciones)
SoundCloud (instrucciones)
VK (instrucciones)

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 => { ... });

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información
de solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta
información incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones
no leen automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.
Conservar notificaciones adicionales y los tokens de
proveedores externos en ASP.NET Core
19/05/2019 • 13 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)

Requisitos previos
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. La aplicación de ejemplo usa el
proveedor de autenticación de Google.

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 = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

// Provide the Google Client Secret


options.ClientSecret = "{Client Secret}";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");


options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
options.SaveTokens = true;

options.Events.OnCreatingTicket = ctx =>


{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

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

Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize

Twitter https://api.twitter.com/oauth/authenticate

En de la aplicación de ejemplo, Google userinfo.profile ámbito se agrega automáticamente el marco de trabajo


cuando AddGoogle se llama en el AuthenticationBuilder. Si la aplicación requiere ámbitos adicionales, agregue las
opciones. En el ejemplo siguiente, Google https://www.googleapis.com/auth/user.birthday.read ámbito se agrega
con el fin de recuperar la fecha de nacimiento del usuario:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Asignar claves de datos de usuario y crear notificaciones


En las opciones del proveedor, especifique un MapJsonKey o MapJsonSubKey para cada clave o subclave en 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 la configuración regional ( urn:google:locale ) e imagen ( urn:google:picture )
notificaciones a partir de la locale y picture claves en los datos de usuario de Google:

services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

// Provide the Google Client Secret


options.ClientSecret = "{Client Secret}";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");


options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
options.SaveTokens = true;

options.Events.OnCreatingTicket = ctx =>


{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

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 notificaciones para los datos de usuario disponibles en el Principal.

En la aplicación de ejemplo, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establece la configuración


regional ( urn:google:locale ) e imagen ( urn:google:picture ) notificaciones para firmado en ApplicationUser ,
incluida una notificación para GivenName :

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)


{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();

if (info == null)
{
ErrorMessage =
"Error loading external login information during confirmation.";

return RedirectToPage("./Login", new { ReturnUrl = returnUrl });


}

if (ModelState.IsValid)
{
var user = new IdentityUser
{
UserName = Input.Email,
Email = Input.Email
};

var result = await _userManager.CreateAsync(user);

if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);

if (result.Succeeded)
{
// If they exist, add claims to the user for:
// Given (first) name
// Locale
// Picture
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.GivenName));
}

if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))


{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst("urn:google:locale"));
}

if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))


{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst("urn:google:picture"));
}

// Include the access token in the properties


var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);
props.IsPersistent = true;

await _signInManager.SignInAsync(user, props);

_logger.LogInformation(
"User created an account using {Name} provider.",
info.LoginProvider);

return LocalRedirect(returnUrl);
}
}

foreach (var error in result.Errors)


{
ModelState.AddModelError(string.Empty, error.Description);
}
}

LoginProvider = info.LoginProvider;
ReturnUrl = returnUrl;
return Page();
}

De forma predeterminada, las notificaciones de usuario se almacenan en la cookie de autenticación. Si la cookie de


autenticación es demasiado grande, es posible que la aplicación genere un error porque:
El explorador detecta que el encabezado de cookie es demasiado largo.
El tamaño total de la solicitud es demasiado grande.
Si es necesaria para procesar las solicitudes de usuario una gran cantidad de datos de usuario:
Limitar el número y tamaño de las notificaciones de usuario para que sólo lo que requiere la aplicación de
procesamiento de solicitudes.
Usar una personalizada ITicketStore para el Middleware de autenticación de cookies SessionStore para
almacenar la identidad a través de solicitudes. Conservar grandes cantidades de información de identidad en el
servidor al enviar sólo una pequeña identificador de clave de sesión al cliente.

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 = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

// Provide the Google Client Secret


options.ClientSecret = "{Client Secret}";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");


options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
options.SaveTokens = true;

options.Events.OnCreatingTicket = ctx =>


{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

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 el token de acceso en OnPostConfirmationAsync (nuevo registro de usuario) y
OnGetCallbackAsync (usuario registrado anteriormente) en Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)


{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();

if (info == null)
{
ErrorMessage =
"Error loading external login information during confirmation.";

return RedirectToPage("./Login", new { ReturnUrl = returnUrl });


}
}

if (ModelState.IsValid)
{
var user = new IdentityUser
{
UserName = Input.Email,
Email = Input.Email
};

var result = await _userManager.CreateAsync(user);

if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);

if (result.Succeeded)
{
// If they exist, add claims to the user for:
// Given (first) name
// Locale
// Picture
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.GivenName));
}

if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))


{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst("urn:google:locale"));
}

if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))


{
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst("urn:google:picture"));
}

// Include the access token in the properties


var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);
props.IsPersistent = true;

await _signInManager.SignInAsync(user, props);

_logger.LogInformation(
"User created an account using {Name} provider.",
info.LoginProvider);

return LocalRedirect(returnUrl);
}
}

foreach (var error in result.Errors)


{
ModelState.AddModelError(string.Empty, error.Description);
}
}

LoginProvider = info.LoginProvider;
ReturnUrl = returnUrl;
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 = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

// Provide the Google Client Secret


options.ClientSecret = "{Client Secret}";
// Register with User Secrets using:
// dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");


options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
options.SaveTokens = true;

options.Events.OnCreatingTicket = ctx =>


{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});

ctx.Properties.StoreTokens(tokens);

return Task.CompletedTask;
};
});

Creación y la incorporación de notificaciones


El marco de trabajo proporciona acciones comunes y métodos de extensión para crear y agregar notificaciones a la
colección. Para obtener más información, vea ClaimActionCollectionMapExtensions y
ClaimActionCollectionUniqueExtensions.
Los usuarios pueden definir las acciones personalizadas mediante la derivación de ClaimAction e implementación
abstracta Run método.
Para obtener más información, consulta Microsoft.AspNetCore.Authentication.OAuth.Claims.

Eliminación de las acciones de notificación y notificaciones


ClaimActionCollection.Remove(String) quita todas las notificaciones de acciones para el determinado ClaimType de
la colección. ClaimActionCollectionMapExtensions.DeleteClaim (ClaimActionCollection, String) elimina una
notificación de la dada ClaimType de la identidad. DeleteClaim se usa principalmente con OpenID Connect (OIDC )
para quitar las notificaciones generadas por el protocolo.

Salida de la aplicación de ejemplo


User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
someone@gmail.com
AspNet.Identity.SecurityStamp
7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
Judy
urn:google:locale
en
urn:google:picture
https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
Bearer
.Token.expires_at
2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
4/11/2019 9:14:52 PM
.TokenNames
access_token;token_type;expires_at;TicketCreated
.persistent
.issued
Thu, 11 Apr 2019 20:51:06 GMT
.expires
Thu, 25 Apr 2019 20:51:06 GMT

Reenvío de información de solicitud con un servidor proxy o un


equilibrador de carga
Si la aplicación se implementa detrás de un servidor proxy o de un equilibrador de carga, parte de la información de
solicitud original podría reenviarse a la aplicación en los encabezados de solicitud. Normalmente, esta información
incluye el esquema de solicitud seguro ( https ), el host y la dirección IP del cliente. Las aplicaciones no leen
automáticamente estos encabezados de solicitud para detectar y usar la información de solicitud original.
El esquema se usa en la generación de vínculos que afecta al flujo de autenticación con proveedores externos. El
resultado de perder el esquema seguro ( https ) es que la aplicación genera direcciones URL incorrectas poco
seguras.
Use middleware de encabezados reenviados para que la información de solicitud original esté disponible para la
aplicación para procesar las solicitudes.
Para obtener más información, vea Configuración de ASP.NET Core para trabajar con servidores proxy y
equilibradores de carga.

Recursos adicionales
aplicación SocialSample ingeniería de ASPNET/AspNetCore – es la aplicación de ejemplo vinculado en el del
repositorio de GitHub de aspnet/AspNetCore master rama ingeniería. El master rama contiene código en
desarrollo activo para la próxima versión de ASP.NET Core. Para ver una versión de la aplicación de ejemplo
para una versión comercial de ASP.NET Core, use el rama lista desplegable lista para seleccionar una rama de
versión (por ejemplo release/2.2 ).
Esquemas de directivas en ASP.NET Core
10/05/2019 • 3 minutes to read • Edit Online

Esquemas de la directiva de autenticación facilitan tiene un esquema de autenticación lógico único potencialmente
usar varios enfoques. Por ejemplo, un esquema de la directiva podría usar autenticación de Google para enfrentar
los desafíos y autenticación de cookies para todo lo demás. Esquemas de autenticación de directiva hacen que sea:
Fácil reenviar cualquier acción de autenticación a otro esquema.
Avance de manera dinámica basándose en la solicitud.
Todos los esquemas de autenticación que utilice derivados AuthenticationSchemeOptions y asociado
AuthenticationHandler<TOptions> :

Son automáticamente combinaciones de directivas en ASP.NET Core 2.1 y versiones posteriores.


Puede habilitarse a través de configuración de las opciones del esquema.

public class AuthenticationSchemeOptions


{
/// <summary>
/// If set, this specifies a default scheme that authentication handlers should
/// forward all authentication operations to, by default. The default forwarding
/// logic checks in this order:
/// 1. The most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut
/// 2. The ForwardDefaultSelector
/// 3. ForwardDefault
/// The first non null result is used as the target scheme to forward to.
/// </summary>
public string ForwardDefault { get; set; }

/// <summary>
/// If set, this specifies the target scheme that this scheme should forward
/// AuthenticateAsync calls to. For example:
/// Context.AuthenticateAsync("ThisScheme") =>
/// Context.AuthenticateAsync("ForwardAuthenticateValue");
/// Set the target to the current scheme to disable forwarding and allow
/// normal processing.
/// </summary>
public string ForwardAuthenticate { get; set; }

/// <summary>
/// If set, this specifies the target scheme that this scheme should forward
/// ChallengeAsync calls to. For example:
/// Context.ChallengeAsync("ThisScheme") =>
/// Context.ChallengeAsync("ForwardChallengeValue");
/// Set the target to the current scheme to disable forwarding and allow normal
/// processing.
/// </summary>
public string ForwardChallenge { get; set; }

/// <summary>
/// If set, this specifies the target scheme that this scheme should forward
/// ForbidAsync calls to.For example:
/// Context.ForbidAsync("ThisScheme")
/// => Context.ForbidAsync("ForwardForbidValue");
/// Set the target to the current scheme to disable forwarding and allow normal
/// processing.
/// </summary>
public string ForwardForbid { get; set; }

/// <summary>
/// <summary>
/// If set, this specifies the target scheme that this scheme should forward
/// SignInAsync calls to. For example:
/// Context.SignInAsync("ThisScheme") =>
/// Context.SignInAsync("ForwardSignInValue");
/// Set the target to the current scheme to disable forwarding and allow normal
/// processing.
/// </summary>
public string ForwardSignIn { get; set; }

/// <summary>
/// If set, this specifies the target scheme that this scheme should forward
/// SignOutAsync calls to. For example:
/// Context.SignOutAsync("ThisScheme") =>
/// Context.SignInAsync("ForwardSignOutValue");
/// Set the target to the current scheme to disable forwarding and allow normal
/// processing.
/// </summary>
public string ForwardSignOut { get; set; }

/// <summary>
/// Used to select a default scheme for the current request that authentication
/// handlers should forward all authentication operations to by default. The
/// default forwarding checks in this order:
/// 1. The most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut
/// 2. The ForwardDefaultSelector
/// 3. ForwardDefault.
/// The first non null result will be used as the target scheme to forward to.
/// </summary>
public Func<HttpContext, string> ForwardDefaultSelector { get; set; }
}

Ejemplos
El ejemplo siguiente muestra un esquema de nivel superior que combina los esquemas de nivel inferior. Se utiliza
la autenticación de Google para enfrentar los desafíos y se usa la autenticación de cookies para todo lo demás:

public void ConfigureServices(IServiceCollection services)


{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => options.ForwardChallenge = "Google")
.AddGoogle(options => { });
}

El ejemplo siguiente habilita la selección dinámica de esquemas en función de la solicitud. Es decir, cómo se
combinan las cookies y la autenticación de API.

public void ConfigureServices(IServiceCollection services)


{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// For example, can foward any requests that start with /api
// to the api scheme.
options.ForwardDefaultSelector = ctx =>
ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
})
.AddYourApiAuth("Api");
}
Autenticar a los usuarios con WS-Federation en
ASP.NET Core
10/05/2019 • 7 minutes to read • Edit Online

Este tutorial muestra cómo habilitar 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
de ASP.NET Core 2.0 se describe en Facebook, Google y la autenticación de proveedor externo.
Para las aplicaciones de ASP.NET Core 2.0, proporciona compatibilidad con WS -Federation
Microsoft.AspNetCore.Authentication.WsFederation. Este componente se porta desde
Microsoft.Owin.Security.WsFederation y comparte muchos de los mecanismos de dicho componente. Sin
embargo, los componentes se diferencian en un par de formas importantes.
De forma predeterminada, el middleware nueva:
No permitir inicios de sesión no solicitados. Esta característica del protocolo WS -Federation es vulnerable a
ataques XSRF. Sin embargo, puede habilitarse con el AllowUnsolicitedLogins opción.
No se comprueba cada formulario post para los mensajes de inicio de sesión. Solo las solicitudes a la
CallbackPath se comprueba el inicio de sesión complementos. CallbackPath el valor predeterminado es
/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 para usuario autenticado entidad Asistente para agregar confianza desde la consola de
administración de AD FS:
Elija esta opción escribir manualmente los datos:

Escriba un nombre para mostrar para el usuario de confianza. 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 pasivo de WS -Federation, mediante 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 se 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 con el resto del asistente y cerrar al final.


ASP.NET Core Identity 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 regla, deje el valor predeterminado enviar
atributos LDAP como notificaciones plantilla seleccionada y haga clic en siguiente. Agregue una regla de
asignación el SAM -Account-Name 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 registros de aplicaciones del inquilino AAD. Haga clic en nuevo registro de aplicaciones:

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 escucha en el que el dirección URL de inicio de sesión:
Haga clic en extremos y tenga en cuenta la 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 :
Adición de WS-Federation como proveedor de inicio de sesión externo
para ASP.NET Core Identity
Agregar una dependencia en Microsoft.AspNetCore.Authentication.WsFederation al proyecto.
Agregue el WS -Federation a Startup.ConfigureServices :

services.AddIdentity<IdentityUser, 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 AddDefaultIdentity 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 iniciarla vínculo en el encabezado de la barra de navegación. Hay una opción
para iniciar sesión con WsFederation:

Con ADFS como proveedor, el botón se redirige a una página de inicio de sesión de AD FS:

Con Azure Active Directory como proveedor, el botón se redirige a una página de inicio de sesión de AAD:
Un inicio de sesión correcto para un nuevo usuario redirige a la página de registro de usuario de la aplicación:

Use WS-Federation sin ASP.NET Core Identity


El middleware de WS -Federation se puede usar sin la identidad. Por ejemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["wsfed:realm"];
options.MetadataAddress = Configuration["wsfed:metadata"];
})
.AddCookie();
}

public void Configure(IApplicationBuilder app)


{
app.UseAuthentication();
// …
}
Confirmación de la cuenta y la recuperación de
contraseñas en ASP.NET Core
10/05/2019 • 16 minutes to read • Edit Online

Consulte este archivo PDF para el núcleo de ASP.NET 1.1 y versión 2.1.
Por Rick Anderson, Ponant, 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.2 o posterior

Crear una aplicación web y aplicar la técnica scaffolding identidad


Ejecute los comandos siguientes para crear una aplicación web con la autenticación.

dotnet new webapp -au Individual -uld -o WebPWrecover


cd WebPWrecover
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator identity -dc WebPWrecover.Data.ApplicationDbContext --files
"Account.Register;Account.Login;Account.Logout;Account.ConfirmEmail"
dotnet ef database drop -f
dotnet ef database update
dotnet run

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 Startup.ConfigureServices para requerir un correo electrónico de confirmación:
public void ConfigureServices(IServiceCollection services)
{

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

// requires
// using Microsoft.AspNetCore.Identity.UI.Services;
// using WebPWrecover.Services;
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

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


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 marcado siguiente se muestra el secrets.json archivo. 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.SetClickTracking(false, 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 transitorio.
Registrar el AuthMessageSenderOptions instancia de configuración.
public void ConfigureServices(IServiceCollection services)
{

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

// requires
// using Microsoft.AspNetCore.Identity.UI.Services;
// using WebPWrecover.Services;
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

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 va a iniciar sesión automáticamente en marcando 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:
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. Una vez que se ha restablecido
correctamente su contraseña, puede iniciar sesión con su correo electrónico y la contraseña nueva.

Cambiar el tiempo de espera de correo electrónico y la actividad


El tiempo de espera de inactividad predeterminado es 14 días. El código siguiente establece el tiempo de espera
de inactividad en 5 días:

services.ConfigureApplicationCookie(o => {
o.ExpireTimeSpan = TimeSpan.FromDays(5);
o.SlidingExpiration = true;
});

Cambiar todas las duraciones de token protección de datos


El código siguiente cambia el tiempo de espera de los tokens de protección de datos todas a 3 horas:
public void ConfigureServices(IServiceCollection services)
{

services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromHours(3));

services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Integrado en tokens de identidad de usuario (consulte


AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs ) tiene un tiempo de espera de un día.
Cambiar la duración del token de correo electrónico
La duración del token predeterminado de los tokens de identidad de usuario es un día. En esta sección se muestra
cómo cambiar la duración del token de correo electrónico.
Agregar un personalizado DataProtectorTokenProvider<TUser > y DataProtectionTokenProviderOptions:

public class CustomEmailConfirmationTokenProvider<TUser>


: DataProtectorTokenProvider<TUser> where TUser : class
{
public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
IOptions<EmailConfirmationTokenProviderOptions> options)
: base(dataProtectionProvider, options)
{

}
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
public EmailConfirmationTokenProviderOptions()
{
Name = "EmailDataProtectorTokenProvider";
TokenLifespan = TimeSpan.FromHours(4);
}
}

Agregue el proveedor personalizado para el contenedor de servicios:


public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
new TokenProviderDescriptor(
typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
})
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration); // For SendGrid key.

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Reenviar correo electrónico de confirmación


Consulte este problema de GitHub.
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
10/05/2019 • 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.
Autenticación en dos fases no se realiza mediante un proveedor de autenticación externo, como Google o
Facebook. Inicios de sesión externos están protegidos mediante cualquier mecanismo que proporciona el
proveedor de inicio de sesión externo. Considere, por ejemplo, el Microsoft proveedor de autenticación requiere
una clave de hardware u otro enfoque de 2FA. Si las plantillas predeterminadas aplican 2FA "local", a
continuación, los usuarios se necesitarían para satisfacer dos enfoques 2FA, que no es un escenario frecuente.

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.cs.
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(
AuthenticatorUriFormat,
_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
10/05/2019 • 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.
Vea o descargue el código de ejemplo. 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 HTTPS en ASP.NET Core para configurar y requiere HTTPS.
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:
En 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: En la ficha números, copie 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 (por lo que no hay
ningún marcado está marcada como comentario).

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: 5 minutos bloqueo tras 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
11/06/2019 • 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
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").
La cookie de autenticación IsEssential propiedad está establecida en true de forma predeterminada. Cuando un
visitante del sitio no ha dado su consentimiento para la recopilación de datos, se permiten las cookies de
autenticación. Para obtener más información, consulta Reglamento de protección de datos generales (RGPD ) se
admiten en ASP.NET Core.
En el Configure método, use el UseAuthentication método para invocar el Middleware de autenticación que
establece el HttpContext.Userpropiedad. 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.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.

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.
OPCIÓN DESCRIPCIÓN

Cookie.SecurePolicy Una marca que indica si la cookie creada debe limitarse a


HTTPS ( CookieSecurePolicy.Always ), HTTP o HTTPS (
CookieSecurePolicy.None ), o el mismo protocolo que la
solicitud ( CookieSecurePolicy.SameAsRequest ). El valor
predeterminado es CookieSecurePolicy.SameAsRequest .

DataProtectionProvider Establece el DataProtectionProvider que se 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 .

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 .
OPCIÓN DESCRIPCIÓN

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.

Validate 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 =>
{
...
});

ASP.NET Core 1.x usa cookies middleware que serializa una entidad de seguridad del usuario en una cookie
cifrada. En solicitudes posteriores, la cookie se valida y se vuelve a crear la entidad de seguridad y se asigna a la
HttpContext.User propiedad.

Instalar el Microsoft.AspNetCore.Authentication.Cookies paquete de NuGet en el proyecto. Este paquete contiene


el middleware de cookies.
Use la UseCookieAuthentication método en el Configure método en su Startup.cs archivo antes de UseMvc o
UseMvcWithDefaultRoute :
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AccessDeniedPath = "/Account/Forbidden/",
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
LoginPath = "/Account/Unauthorized/"
});

Opciones de CookieAuthenticationOptions
El CookieAuthenticationOptions clase se usa para configurar las opciones de proveedor de autenticación.

OPCIÓN DESCRIPCIÓN

AuthenticationScheme Establece el esquema de autenticación.


AuthenticationScheme es útil cuando hay varias instancias
de autenticación 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.

AutomaticAuthenticate Establece un valor para indicar que la autenticación con


cookies debe ejecutar en cada solicitud y se intentan validar y
reconstruir a cualquier entidad serializada que creó.

AutomaticChallenge Si es true, el middleware de autenticación controla desafíos


automática. Si es false, el middleware de autenticación solo
modifica las respuestas cuando lo indique explícitamente la
AuthenticationScheme .

ClaimsIssuer El emisor que se usará para la emisor propiedad en las


notificaciones creados por el middleware de autenticación de
cookies.

CookieDomain 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 sirve la cookie para 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 .

CookieHttpOnly 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 .
OPCIÓN DESCRIPCIÓN

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

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

Descripción Información adicional sobre el tipo de autenticación que está


disponible para la aplicación.

ExpireTimeSpan El TimeSpan tras el que expira el vale de autenticación. Se


agrega a la hora actual para crear la hora de expiración para el
vale. Para usar ExpireTimeSpan , debe establecer
IsPersistent a true en el AuthenticationProperties
pasa a SignInAsync . El valor predeterminado es 14 días.

SlidingExpiration Una marca que indica si la fecha de expiración de cookie


restablece cuando haya más de la mitad de la
ExpireTimeSpan ha transcurrido el intervalo. La nueva hora
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 .

Establecer CookieAuthenticationOptions para el Middleware de autenticación de cookies en el Configure método:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
});

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

Secure 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.
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. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.

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

Llame a SignInAsync para iniciar sesión en el usuario:

await HttpContext.Authentication.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));

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
Para cerrar la sesión del usuario actual y eliminar sus cookies, llame a SignOutAsync:

await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
Para cerrar la sesión del usuario actual y eliminar sus cookies, llame a SignOutAsync:

await HttpContext.Authentication.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));

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

Para implementar una invalidación para el ValidateAsync eventos, escribir un método con la firma siguiente:

ValidateAsync(CookieValidatePrincipalContext)

ASP.NET Core Identity implementa esta comprobación como parte de su SecurityStampValidator. Un ejemplo es
similar a la siguiente:
public static class LastChangedValidator
{
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
// Pull database from registered DI services.
var userRepository =
context.HttpContext.RequestServices
.GetRequiredService<IUserRepository>();
var userPrincipal = context.Principal;

// Look for the last changed 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 el evento durante la configuración de autenticación de cookies en el Configure método:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = LastChangedValidator.ValidateAsync
}
});

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 de verificación "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.
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true
});

El AuthenticationProperties clase reside en el Microsoft.AspNetCore.Authentication espacio de nombres.

await HttpContext.Authentication.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true
});

El AuthenticationProperties clase reside en el Microsoft.AspNetCore.Http.Authentication espacio de nombres.

Expiración de cookie absoluta


Puede establecer un tiempo de expiración absoluta con ExpiresUtc . Para crear una cookie persistente, también
debe establecer IsPersistent ; en caso contrario, la cookie se crea con una duración basados en sesión y puede
expirar antes o después de la autenticación de vale que lo contiene. 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.

await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
});

await HttpContext.Authentication.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 en ASP.NET Core
Autorización basada en ASP.NET Core
Comprobaciones de la función basada en directivas
Hospedaje de ASP.NET Core en una granja de servidores web
Usar la autenticación del proveedor de inicio de
sesión social sin ASP.NET Core Identity
04/07/2019 • 3 minutes to read • Edit Online

Autenticación con Facebook, Google y proveedores externos en ASP.NET Core Describe cómo habilitar usuarios
iniciar sesión mediante OAuth 2.0 con las credenciales de proveedores de autenticación externos. El enfoque
descrito en este tema incluye ASP.NET Core Identity como proveedor de autenticación.
Este ejemplo muestra cómo usar el proveedor de autenticación externo sin ASP.NET Core Identity. Esto es útil
para las aplicaciones que no requieren todas las características de ASP.NET Core Identity, pero seguirá necesitan la
integración con un proveedor de autenticación externo de confianza.
Este ejemplo utiliza autenticación de Google para autenticar a los usuarios. Autenticación con Google desplaza
muchas de las complejidades de administrar el proceso de inicio de sesión de Google. Para integrar con un
proveedor de autenticación externo diferente, vea los temas siguientes:
Autenticación con Facebook
Autenticación con Microsoft
Autenticación con Twitter
Otros proveedores

Configuración
En el método, configure los esquemas de autenticación de la aplicación con el
ConfigureServices
AddAuthentication , AddCookie y AddGoogle métodos:

public void ConfigureServices(IServiceCollection services)


{
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

La llamada a AddAuthentication establece la aplicación DefaultScheme. El DefaultScheme es el esquema


predeterminado para las siguientes HttpContext métodos de extensión de autenticación:
AuthenticateAsync
ChallengeAsync
ForbidAsync
SignInAsync
SignOutAsync
Configuración de la aplicación DefaultScheme a CookieAuthenticationDefaults.AuthenticationScheme ("Cookies")
configura la aplicación para usar las Cookies que el esquema predeterminado para estos métodos de extensión.
Configuración de la aplicación DefaultChallengeScheme a GoogleDefaults.AuthenticationScheme ("Google")
configura la aplicación para usar Google como el esquema predeterminado para las llamadas a ChallengeAsync .
DefaultChallengeScheme invalida DefaultScheme . Consulte AuthenticationOptions de propiedades adicionales que
reemplazan DefaultScheme cuando se establece.
En el Configure método, llame a la 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();

Para obtener más información acerca de los esquemas de autenticación y autenticación con cookies, consulte Usar
autenticación de cookies sin ASP.NET Core Identity.

Aplicar la autorización
Probar la configuración de autenticación de la aplicación aplicando la AuthorizeAttribute atributo a un
controlador, acción o página. El siguiente código limita el acceso a la privacidad página a los usuarios que han sido
autenticados:

[Authorize]
public class PrivacyModel : PageModel
{
public void OnGet()
{
}
}

Cerrar sesión
Para cerrar la sesión del usuario actual y eliminar sus cookies, llame a SignOutAsync. El código siguiente agrega un
Logout controlador de páginas la índice página:

public class IndexModel : PageModel


{
public void OnGet()
{
}

public async Task<IActionResult> OnPostLogoutAsync()


{
await HttpContext.SignOutAsync();
return RedirectToPage();
}
}

Tenga en cuenta que la llamada a SignOutAsync no especifica un esquema de autenticación. La aplicación


DefaultScheme de CookieAuthenticationDefaults.AuthenticationScheme se utiliza como un retroceso.

Recursos adicionales
Autorización simple en ASP.NET Core
Conservar notificaciones adicionales y los tokens de proveedores externos en ASP.NET Core
Azure Active Directory con ASP.NET Core
10/05/2019 • 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 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
03/07/2019 • 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 2019

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

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


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

Configure las opciones de cookies o


JwtBearer/OpenIdConnectOptions subyacentes
Para configurar las opciones subyacentes directamente, usar la constante de esquema adecuado en
Startup.ConfigureServices :
services.Configure<OpenIdConnectOptions>(
AzureAD[B2C]Defaults.OpenIdScheme, options =>
{
// Omitted for brevity
});

services.Configure<CookieAuthenticationOptions>(
AzureAD[B2C]Defaults.CookieScheme, options =>
{
// Omitted for brevity
});

services.Configure<JwtBearerOptions>(
AzureAD[B2C]Defaults.JwtBearerAuthenticationScheme, options =>
{
// Omitted for brevity
});

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 web API con Azure Active Directory
B2C en ASP.NET Core
14/05/2019 • 17 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. B2C de Azure AD también proporciona la autenticación multifactor con una configuración mínima.
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 2019
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 se
registró anteriormente. 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


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 individuales >
conectar a un almacén de usuario existente en la nube.

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 obtiene los tokens desde 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. Por ejemplo:
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 recibe 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 del


https://login.microsoftonline.com/{tenant
domain name}/oauth2/v2.0/authorize? inquilino } con el nombre de dominio
p=B2C_1_SiUpIn
del inquilino. IMPORTANTE: Esta
dirección URL debe tener el mismo
nombre de dominio que lo que se
encuentra en AzureAdB2C.Instance
en la API web appsettings.json
archivo. Vea la nota†.

Id. de cliente {Escriba la aplicación Postman Id.


de aplicación}

Ámbito https://{tenant domain Reemplace {nombre de dominio del


name}/{api}/user_impersonation inquilino } con el nombre de dominio
openid offline_access
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/{api-
id-uri}/{scope name}
.

Estado {Deje en blanco }

Autenticación de cliente Enviar las credenciales del cliente en


el cuerpo

NOTE
† El cuadro de diálogo de configuración de directiva en el portal de Azure Active Directory B2C muestra dos
direcciones URL posibles: Uno con el formato https://login.microsoftonline.com/ {nombre de dominio del
inquilino} / {información de ruta de acceso adicional} y otro en el formato https://{tenant name}.b2clogin.com/
{nombre de dominio del inquilino} / {información de ruta de acceso adicional}. Tiene críticos que el dominio se
encuentra en AzureAdB2C.Instance en la API web appsettings.json archivo coincida con el usado en la aplicación
web appsettings.json archivo. Se trata del mismo dominio que se utiliza para el campo de dirección URL de
autenticación en Postman. Tenga en cuenta que Visual Studio usa un formato de dirección URL ligeramente distinto a
lo que se muestra en el portal. Siempre que coincidan con los dominios, la dirección URL funciona.

3. Seleccione el solicitar Token botón.


4. Postman abre una nueva ventana que contiene el diálogo de inicio de sesión del inquilino 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.
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
10/05/2019 • 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 webapp -au Individual

dotnet new mvc -au Individual


dotnet new razor -au Individual

Consulte este problema de GitHub para la autenticación de API web.

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
Información general
03/07/2019 • 8 minutes to read • Edit Online

Microsoft.AspNetCore.Authentication.Certificate contiene una implementación similar a autenticación de


certificados para ASP.NET Core. Autenticación de certificado se produce en el nivel TLS, mucho antes de que
alguna vez se llegue a ASP.NET Core. Más concretamente, esto es un controlador de autenticación que valida el
certificado y, a continuación, le ofrece un evento que puede resolver dicho certificado para un ClaimsPrincipal .
Configurar el host para la autenticación de certificado, ya sea IIS, Kestrel, Azure Web Apps, o cualquier otra cosa
que esté usando.

Primeros pasos
Adquirir un certificado HTTPS, aplicarla, y configurar el host para requerir certificados.
En la aplicación web, agregue una referencia a la Microsoft.AspNetCore.Authentication.Certificate paquete. A
continuación, en el Startup.Configure método, llame a
app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);
con sus opciones, que proporciona un delegado para OnCertificateValidated para realizar cualquier validación
adicional en el certificado de cliente enviado con las solicitudes. Convertir esa información en un ClaimsPrincipal
y establézcalo en la context.Principal propiedad.
Si se produce un error de autenticación, este controlador devuelve un 403 (Forbidden) respuesta en su lugar un
401 (Unauthorized) , como cabría esperar. El razonamiento es que la autenticación debe ocurrir durante la conexión
inicial de TLS. Cuando llega el controlador, es demasiado tarde. No hay ninguna manera para actualizar la conexión
de una conexión anónima a uno con un certificado.
También agregar app.UseAuthentication(); en el Startup.Configure método. En caso contrario, no se establecerán
HttpContext.User ClaimsPrincipal creado a partir del certificado. Por ejemplo:

public void ConfigureServices(IServiceCollection services)


{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
// All the other service configuration.
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)


{
app.UseAuthentication();

// All the other app configuration.


}

El ejemplo anterior muestra la manera predeterminada para agregar la autenticación de certificado. El controlador
crea a una entidad de usuario con las propiedades comunes del certificado.

Configurar la validación del certificado


El CertificateAuthenticationOptions controlador tiene algunas validaciones integradas que están las validaciones
mínimas que debe realizar en un certificado. Cada una de estas opciones está habilitada de forma predeterminada.
AllowedCertificateTypes = encadenadas, SelfSigned o todos (encadenados | SelfSigned)
Esta comprobación se valida que se permite solo el tipo de certificado adecuado.
ValidateCertificateUse
Esta comprobación se valida que el certificado presentado por el cliente tiene la autenticación del cliente amplía el
uso de claves (EKU ), o ninguna EKU en absoluto. Según las especificaciones por ejemplo, si no se especifica ningún
EKU, todos los EKU se consideran válidos.
ValidateValidityPeriod
Esta comprobación se valida que el certificado está dentro de su período de validez. En cada solicitud, el
controlador garantiza que no ha expirado un certificado que tenía cuando se presentó durante su sesión actual.
RevocationFlag
Una marca que especifica qué certificados de la cadena se comprueba la revocación.
Solo se realizan comprobaciones de revocación cuando el certificado está enlazado a un certificado raíz.
RevocationMode
Una marca que especifica cómo se realizan las comprobaciones de revocación.
Especificar una comprobación en línea puede provocar un retraso largo mientras se contacta con la entidad
emisora de certificados.
Solo se realizan comprobaciones de revocación cuando el certificado está enlazado a un certificado raíz.
¿Configurar mi aplicación para solicitar un certificado solo en determinadas rutas de acceso?
Esto no es posible. Recuerde que se realiza el intercambio de certificados que el inicio de la conversación de
HTTPS, ya está listo el servidor antes de que se recibe la primera solicitud en esa conexión, por lo que no es posible
con ámbito basado en los campos de la solicitud.

Eventos
El controlador tiene dos eventos:
OnAuthenticationFailed – Se llama si se produce durante la autenticación de una excepción y le permite
reaccionar.
OnCertificateValidated – Se llama después de que se validó el certificado pasó la validación y se ha creado una
entidad de seguridad predeterminada. Este evento permite realizar su propia validación y aumentar o
reemplazar la entidad de seguridad. Para obtener ejemplos incluyen:
Determinar si el certificado se conoce a los servicios.
Creación de su propia entidad de seguridad. Considere el ejemplo siguiente de
Startup.ConfigureServices :
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};

context.Principal = new ClaimsPrincipal(


new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();

return Task.CompletedTask;
}
};
});

Si encuentra el certificado de entrada no cumple su validación adicional, llame a context.Fail("failure reason")


con un motivo del error.
Funcionalidad para el real, probablemente quiera llamar a un servicio registrado en la inserción de dependencias
que se conecta a una base de datos u otro tipo de almacén de usuario. Acceso al servicio utilizando el contexto que
se pasa el delegado. Considere el ejemplo siguiente de Startup.ConfigureServices :
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices
.GetService<ICertificateValidationService>();

if (validationService.ValidateCertificate(
context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};

context.Principal = new ClaimsPrincipal(


new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}

return Task.CompletedTask;
}
};
});

Conceptualmente, la validación del certificado es una preocupación de autorización. Agregar una comprobación,
por ejemplo, un emisor o huella digital en una directiva de autorización, en lugar de inside OnCertificateValidated ,
es perfectamente aceptable.

Configurar el host para requerir certificados


Kestrel
En Program.cs, configure Kestrel como sigue:

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


WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(opt =>
opt.ClientCertificateMode =
ClientCertificateMode.RequireCertificate);
})
.Build();

IIS
Complete los pasos siguientes en el Administrador de IIS:
1. Seleccione el sitio desde el conexiones ficha.
2. Haga doble clic en el configuración SSL opción el vista características ventana.
3. Compruebe el requerir SSL casilla de verificación y seleccione el requieren botón de radio en el certificados
de cliente sección.

Azure y servidores proxy web personalizada


Consulte la hospedaje e implementación documentación acerca de cómo configurar el certificado de middleware
de reenvío.
Introducción a la autorización en ASP.NET Core
10/05/2019 • 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 se permite para crear una biblioteca de documentos, documentos de agregar, editar documentos y
eliminarlos. Un usuario sin derechos administrativos, trabajar con la biblioteca solo está autorizado para leer los
documentos.
La autorización es ortogonal e 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 más identidades para el usuario actual.

Tipos de autorización
Autorización de ASP.NET Core proporciona un sencillo, de manera declarativo rol y un variado basada en
directivas modelo. Autorización se expresa en los requisitos y controladores de evaluación las notificaciones de
un usuario con los requisitos. Comprobaciones imperativas pueden basarse en las directivas de simple o que
evaluar la identidad del usuario y las propiedades del recurso al que el usuario está intentando obtener acceso.

Espacios de nombres
Componentes de autorización, incluido el AuthorizeAttribute y AllowAnonymousAttribute atributos, se
encuentran en el Microsoft.AspNetCore.Authorization espacio de nombres.
Consulte la documentación en autorización sencilla.
Crear una aplicación ASP.NET Core con datos de
usuario protegidos por autorización
10/05/2019 • 28 minutes to read • Edit Online

Por Rick Anderson y Joe Audette


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
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 : 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 ASP.NET Core 2.2 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);

if(user == null)
{
throw new Exception("The testUserPw password was probably not strong enough!");
}

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 ya no ha establecido una contraseña para cuentas de usuario inicializados, utilice el herramienta Secret
Manager para establecer una contraseña:
Elija una contraseña segura: Use ocho o más caracteres y al menos un carácter en mayúsculas, números y
símbolos. Por ejemplo, Passw0rd! cumple los requisitos de contraseña segura.
Ejecute el siguiente comando desde la carpeta del proyecto, donde <PW> es la contraseña:

dotnet user-secrets set SeedUserPW <PW>

Si la aplicación tiene contactos:


Eliminar todos los registros en el Contact tabla.
Reinicie la aplicación para inicializar la base de datos.
Es una manera fácil de probar la aplicación completa iniciar los tres distintos exploradores (o las sesiones 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:
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 PROPAGADAS POR LA APLICACIÓN OPCIONES

test@example.com No Editar o eliminar los datos propios.

manager@contoso.com Sí Aprobar o rechazar y editar o eliminar


los datos propios.

admin@contoso.com Sí Aprobar o rechazar y editar o eliminar


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:

public class Contact


{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}

Scaffold el Contact modelo.


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
Compilar una aplicación web de .NET Core y SQL Database en Azure App Service
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.
Introducción a la autorización en ASP.NET Core
Autorización personalizada basada en directivas
Convenciones de autorización de las páginas de
Razor en ASP.NET Core
10/05/2019 • 6 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. Los conceptos y ejemplos que
se muestran en este tema se aplican igualmente a las aplicaciones que usan ASP.NET Core Identity. Para usar
ASP.NET Core Identity, siga las instrucciones de Introducción a la identidad en ASP.NET Core.

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

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.
Para especificar un directiva de autorización, utilice un AuthorizePage sobrecarga:

options.Conventions.AuthorizePage("/Contact", "AtLeast21");

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

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.
Para especificar un directiva de autorización, utilice un AuthorizeFolder sobrecarga:

options.Conventions.AuthorizeFolder("/Private", "AtLeast21");

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/.
Para especificar un directiva de autorización, utilice un AuthorizeAreaPage sobrecarga:

options.Conventions.AuthorizeAreaPage("Identity", "/Manage/Accounts", "AtLeast21");

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.
Para especificar un directiva de autorización, utilice un AuthorizeAreaFolder sobrecarga:

options.Conventions.AuthorizeAreaFolder("Identity", "/Manage", "AtLeast21");

Permitir el acceso anónimo a una página


Use la AllowAnonymousToPage convención a través de AddRazorPagesOptions para agregar una
AllowAnonymousFilter a una página en la ruta 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_2);

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

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 válido para especificar que una carpeta de páginas que requieren autorización y de especificar que una página
dentro de esa carpeta permite el acceso anónimo:

// This works.
.AuthorizeFolder("/Private").AllowAnonymousToPage("/Private/Public")

Sin embargo, la inversa, no es válida. No se puede declarar una carpeta de páginas para el acceso anónimo y, a
continuación, especifique una página dentro de esa carpeta que requiere autorización:

// This doesn't work!


.AllowAnonymousToFolder("/Public").AuthorizePage("/Public/Private")

Se produce un error que requiere autorización en la página privada. Cuando tanto el AllowAnonymousFilter y
AuthorizeFilter se aplican a la página, el AllowAnonymousFilter tiene prioridad y controla el acceso.

Recursos adicionales
Convenciones de aplicación y de ruta de páginas de Razor en ASP.NET Core
PageConventionCollection
Autorización simple en ASP.NET Core
10/05/2019 • 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 simple, 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
atributo a la propia acción:

public class AccountController : Controller


{
public ActionResult Login()
{
}

[Authorize]
public ActionResult Logout()
{
}
}

Ahora solo los usuarios autenticados pueden tener acceso el Logout función.
También puede usar el AllowAnonymous atributo para permitir el acceso a los usuarios no autenticados a
acciones individuales. Por ejemplo:

[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}

public ActionResult Logout()


{
}
}

Esto permitiría que solo los usuarios autenticados para la AccountController , excepto para el Login acción, que
es accesible para todo el mundo, independientemente de su estado autenticado o no autenticado o anónimo.

WARNING
[AllowAnonymous] omite todas las instrucciones de autorización. Si combina [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
10/05/2019 • 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.

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()
{
}
}

Para las páginas de Razor, el AuthorizeAttribute puede aplicarse, ya sea por:


Mediante un convención, o
Aplicar el AuthorizeAttribute a la PageModel instancia:

[Authorize(Policy = "RequireAdministratorRole")]
public class UpdateModel : PageModel
{
public ActionResult OnPost()
{
}
}

IMPORTANT
Filtrar los atributos, como AuthorizeAttribute , sólo puede aplicarse a PageModel y no se puede aplicar a métodos de
controlador de página específica.

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.
Agregar servicios de función a la identidad
Anexar AddRoles para agregar servicios de rol:

public void ConfigureServices(IServiceCollection services)


{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Autorización basada en ASP.NET Core
10/05/2019 • 6 minutes to read • Edit Online

Cuando se crea una identidad se pueden asignar una o más notificaciones emitidas por una entidad de confianza.
Una notificación es un par nombre-valor que representa el asunto de qué es, no lo que el sujeto puede hacer. Por
ejemplo, puede tener un permiso de conducir, emitido por una entidad de licencia de conducción local. De conducir
su permiso tiene su fecha de nacimiento. En este caso sería el nombre de la notificación DateOfBirth , el valor de
notificación sería su fecha de nacimiento, por ejemplo 8th June 1970 y el emisor sería la autoridad de licencia de
conducción. Autorización basada en notificaciones, en su forma más simple, comprueba el valor de una
notificación y permite el acceso a un recurso en función de ese valor. Por ejemplo, si desea que el proceso de
autorización el acceso a un club nocturno puede ser:
El responsable de la seguridad de la puerta evaluaría el valor de la fecha de nacimiento notificación y si confía en
el emisor (la entidad de licencia conducción) antes de conceder que acceso.
Una identidad puede contener varias notificaciones con varios valores y puede contener varias notificaciones del
mismo tipo.

Agregar comprobaciones de notificaciones


Notificación de comprobaciones de autorización basado en son declarativas: el desarrollador incrusta dentro de su
código, con un controlador o una acción dentro de un controlador, especificar las 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 de los 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.
Primero deberá crear y registrar la directiva. Esta operación se realiza como parte de la configuración del servicio
de autorización, que normalmente forma parte de 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, aplicar 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 puede aplicarse a un controlador todo, en este caso solo las identidades de la
directiva de coincidencia se permitirá acceso a cualquier acción en el controlador.

[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
public ActionResult VacationBalance()
{
}
}

Si tiene un controlador que está protegido por la 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 las notificaciones incluyen un valor. Puede especificar una lista de valores permitidos al crear la
directiva. El ejemplo siguiente se realizaría correctamente sólo 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"));
});
}

Agregar una comprobación de solicitud 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 mediante func para satisfacer una directiva.

Evaluación de directiva múltiples


Si varias directivas se aplican a un controlador o acción, todas las directivas deben pasar antes de que se concede
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 poder 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 él, a continuación, comprobar la edad es 21 o una versión anterior, deberá escribir controladores de
directiva personalizada.
Autorización basada en directivas en ASP.NET
Core
12/06/2019 • 16 minutes to read • Edit Online

Interiormente, autorización basada en roles y autorización basada en notificaciones usan 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
completas, reutilizables y apta para las pruebas.
Una directiva de autorización consta de uno o más requisitos. Está registrado como parte de la configuración
del servicio de autorización, en el Startup.ConfigureServices método:

public void ConfigureServices(IServiceCollection services)


{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

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 suministra como parámetro al requisito.

IAuthorizationService
El servicio principal que determina si la autorización sea correcta es IAuthorizationService:
/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
/// <summary>
/// Checks if a user meets a specific set of requirements for the specified resource
/// </summary>
/// <param name="user">The user to evaluate the requirements against.</param>
/// <param name="resource">
/// An optional resource the policy should be checked with.
/// If a resource is not required for policy evaluation you may pass null as the value
/// </param>
/// <param name="requirements">The requirements to evaluate.</param>
/// <returns>
/// A flag indicating whether authorization has succeeded.
/// This value is <value>true</value> when the user fulfills the policy;
/// otherwise <value>false</value>.
/// </returns>
/// <remarks>
/// Resource is an optional parameter and may be null. Please ensure that you check
/// it is not null before acting upon it.
/// </remarks>
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource,
IEnumerable<IAuthorizationRequirement> requirements);

/// <summary>
/// Checks if a user meets a specific authorization policy
/// </summary>
/// <param name="user">The user to check the policy against.</param>
/// <param name="resource">
/// An optional resource the policy should be checked with.
/// If a resource is not required for policy evaluation you may pass null as the value
/// </param>
/// <param name="policyName">The name of the policy to check against a specific
/// context.</param>
/// <returns>
/// A flag indicating whether authorization has succeeded.
/// Returns a flag indicating whether the user, and optional resource has fulfilled
/// the policy.
/// <value>true</value> when the policy has been fulfilled;
/// otherwise <value>false</value>.
/// </returns>
/// <remarks>
/// Resource is an optional parameter and may be null. Please ensure that you check
/// it is not null before acting upon it.
/// </remarks>
Task<AuthorizationResult> AuthorizeAsync(
ClaimsPrincipal user, object resource, string policyName);
}

El código anterior resalta los dos métodos de la IAuthorizationService.


IAuthorizationRequirement es un servicio de marcador con ningún método y el mecanismo para realizar el
seguimiento de si la autorización sea correcta.
Cada IAuthorizationHandler es responsable de comprobar si se cumplen los requisitos:
/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
/// <summary>
/// Makes a decision if authorization is allowed.
/// </summary>
/// <param name="context">The authorization information.</param>
Task HandleAsync(AuthorizationHandlerContext context);
}

La AuthorizationHandlerContext clase es lo que el controlador que se usa para marcar si se cumplen los
requisitos:

context.Succeed(requirement)

El código siguiente muestra la simplificada (y anotado con comentarios) predeterminado de la


implementación del servicio de autorización:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,


object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
// Create a tracking context from the authorization inputs.
var authContext = _contextFactory.CreateContext(requirements, user, resource);

// By default this returns an IEnumerable<IAuthorizationHandlers> from DI.


var handlers = await _handlers.GetHandlersAsync(authContext);

// Invoke all handlers.


foreach (var handler in handlers)
{
await handler.HandleAsync(authContext);
}

// Check the context, by default success is when all requirements have been met.
return _evaluator.Evaluate(authContext);
}

El código siguiente muestra una típica ConfigureServices :

public void ConfigureServices(IServiceCollection services)


{
// Add all of your handlers to DI.
services.AddSingleton<IAuthorizationHandler, MyHandler1>();
// MyHandler2, ...

services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

// Configure your policies


services.AddAuthorization(options =>
options.AddPolicy("Something",
policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Use IAuthorizationService o [Authorize(Policy = "Something"] para la autorización.


Aplicar directivas a los controladores MVC
Si está usando las páginas de Razor, consulte aplicar directivas a las páginas de Razor en este documento.
Las directivas se aplican a los controladores mediante el uso de la [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 Index() => View();
}

Aplicar directivas a las páginas de Razor


Las directivas se aplican a las páginas de Razor mediante el uso de la [Authorize] atributo con el nombre de
la directiva. Por ejemplo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

Las directivas pueden aplicarse también a las páginas de Razor mediante el uso de un convención
autorización.

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
único parámetro—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; }

public MinimumAgeRequirement(int minimumAge)


{
MinimumAge = minimumAge;
}
}

Si una directiva de autorización contiene varios requisitos de autorización, deben pasar todos los requisitos
para la evaluación de directivas se realice correctamente. En otras palabras, se tratan varios requisitos de
autorización que se agrega a una sola directiva de autorización en un AND base.
NOTE
Un requisito no debe tener 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 en relación con proporcionado
AuthorizationHandlerContext para determinar si se permite el acceso.
Puede tener un requisito varios controladores. Puede heredar un controlador
AuthorizationHandler<TRequirement >, donde TRequirement es el requisito para que lo administre. 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 utiliza
un requisito único:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

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 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 completada. Cuando hay una notificación, se calcula la edad
del usuario. Si el usuario cumple 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 uno a varios en el que un controlador de permiso puede controlar
los tres tipos diferentes de los requisitos:

using System.Linq;
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;
}
}
El código anterior recorre PendingRequirements—una propiedad que contiene los requisitos no marcada
como correcta. Para un ReadPermission requisito, el usuario debe ser un propietario o un patrocinador para
acceder al recurso solicitado. En el caso de un EditPermission o DeletePermission requisito quien debe ser
un propietario para acceder al recurso solicitado.
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().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

El código anterior registra como un singleton invocando


MinimumAgeHandler
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); . Los controladores pueden registrarse
mediante cualquiera de los integrados duraciones de servicio.

¿Qué debe devolver un controlador?


Tenga en cuenta que el Handle método en el ejemplo controlador no devuelve ningún valor. ¿Cómo es un
estado de éxito o error indicado?
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, ya que otros controladores para el
mismo requisito pueden ser correcto.
Para garantizar el error, incluso si otros controladores de requisitos se realice correctamente, llame a
context.Fail .

Si llama un controlador context.Succeed o context.Fail , todavía se llama a todos los otros controladores.
Esto permite que los requisitos producir efectos secundarios, como el registro, que tiene lugar incluso si otro
controlador ha validado correctamente o bien no un requisito. Cuando se establece en false ,
InvokeHandlersAfterFailure propiedad (disponible en ASP.NET Core 1.1 y versiones posterior) provoca un
cortocircuito en la ejecución de controladores cuando context.Fail se llama. InvokeHandlersAfterFailure el
valor predeterminado es true , en cuyo caso se llama a todos los controladores.

NOTE
Incluso si se produce un error de autenticación, se denominan controladores de autorización.

¿Por qué querría varios controladores para un requisito?


En los casos donde desee que la evaluación en un o base, implementar varios controladores para un requisito
único. Por ejemplo, Microsoft tiene puertas que abre sólo con las tarjetas de clave. Si deja la tarjeta de claves
en casa, la recepcionista imprime un adhesivo temporal y abre la puerta. 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 System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>


{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
BuildingEntryRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "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 System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>


{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
BuildingEntryRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "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.

Uso de func para satisfacer una directiva


Puede haber situaciones en que cumplir una directiva es sencilla expresar en 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 escribirse como sigue:

services.AddAuthorization(options =>
{
options.AddPolicy("BadgeEntry", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == "BadgeId" ||
c.Type == "TemporaryBadgeId") &&
c.Issuer == "https://microsoftsecurity")));
});

Acceso al contexto de solicitud MVC en controladores


El HandleRequirementAsync método se implementa en un controlador de autorización tiene dos parámetros:
un AuthorizationHandlerContext y TRequirement está controlando. Marcos de trabajo como MVC o Jabbr son
gratuitas 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 lo contrario, proporcionada por MVC y
páginas de Razor.
El uso de la Resource propiedad es el marco específico. Uso de la información la Resource propiedad limita
sus directivas de autorización para marcos de trabajo determinados. Primero debe convertir el Resource
propiedad mediante la is palabra clave y, a continuación, confirme la conversión se realizó correctamente
para asegurarse de que no se bloquee el código 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
10/05/2019 • 10 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 AspNetCore GitHub. Descargue el archivo ZIP de
repositorio aspnet/AspNetCore. Descomprima el archivo. Navegue hasta la
src/seguridad/samples/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 personalizados de autorización


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 que se utilizará, debe recurrir a un proveedor de copia de
seguridad.
Por ejemplo, considere una aplicación que 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.
El ejemplo IAuthorizationPolicyProviderimplementación mostrado anteriormente se puede actualizar para usar el
DefaultAuthorizationPolicyProvider mediante la creación de un proveedor de directivas de reserva en su
constructor (que se usará en caso de que el nombre de la directiva no coincide con su patrón esperado de
'MinimumAge' + edad).

private DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }

public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)


{
// ASP.NET Core only uses one authorization policy provider, so if the custom implementation
// doesn't handle all policies it should fall back to an alternate provider.
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}

A continuación, la GetPolicyAsync método puede actualizarse para utilizar el FallbackPolicyProvider en lugar de


devolver null:
...
return FallbackPolicyProvider.GetPolicyAsync(policyName);

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, sería conveniente para recuperar la directiva predeterminada de una acción
de reserva IAuthorizationPolicyProvider .

Directiva requerida
Personalizada IAuthorizationPolicyProvider debe implementar GetRequiredPolicyAsync para, opcionalmente,
proporcione una directiva que siempre es necesaria. Si GetRequiredPolicyAsync devuelve una directiva no nula, esa
directiva se combina con cualquier otro (designado o predeterminado) directiva que se solicita.
Si no se necesita ninguna directiva necesaria, el proveedor puede devolver un valor nulo o aplazar el proveedor de
reserva:

public Task<AuthorizationPolicy> GetRequiredPolicyAsync() =>


Task.FromResult<AuthorizationPolicy>(null);

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.AddSingleton<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
10/05/2019 • 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
10/05/2019 • 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.
Vea o descargue el código de ejemplo (cómo descargarlo).
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.

Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,


object resource,
IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user,
object resource,
string policyName);

Task<bool> AuthorizeAsync(ClaimsPrincipal user,


object resource,
IEnumerable<IAuthorizationRequirement> requirements);
Task<bool> 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.

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

[HttpGet]
public async Task<IActionResult> Edit(Guid documentId)
{
Document document = _documentRepository.Find(documentId);

if (document == null)
{
return new NotFoundResult();
}

if (await _authorizationService
.AuthorizeAsync(User, document, "EditPolicy"))
{
return View(document);
}
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. Para obtener más información sobre la creación de una clase de requisito, consulte requisitos.
La clase de controlador especifica el requisito y el tipo de recurso. Por ejemplo, un controlador utilizando un
SameAuthorRequirement y un Document recursos se indica a continuación:

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

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

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

En el ejemplo anterior, imagine que SameAuthorRequirement es un caso especial de un elemento genérico más
SpecificAuthorRequirement clase. El SpecificAuthorRequirement clase (no mostrado) contiene un Name propiedad
que representa el nombre del autor. El Name propiedad puede establecerse en el usuario actual.
Registrar el requisito y el controlador en Startup.ConfigureServices :

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:

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

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

//TODO: Use the following if targeting a version of


//.NET Framework older than 4.6:
// return Task.FromResult(0);
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.

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.

[HttpGet]
public async Task<IActionResult> View(Guid documentId)
{
Document document = _documentRepository.Find(documentId);

if (document == null)
{
return new NotFoundResult();
}

if (await _authorizationService
.AuthorizeAsync(User, document, Operations.Read))
{
return View(document);
}
else
{
return new ChallengeResult();
}
}

Si la autorización se realiza correctamente, se devuelve la vista del documento. Si se produce un error de


autorización, devolver ChallengeResult informa a cualquier middleware de autenticación que error de autorización,
y el software intermedio puede tomar la respuesta adecuada. Una respuesta adecuada podría devolver un código
de estado 401 o 403. Para los clientes de explorador interactivo, podría significar redirigir al usuario a una página
de inicio de sesión.
Autorización basada en la vista en ASP.NET Core
MVC
10/05/2019 • 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 en ASP.NET
Core
10/05/2019 • 6 minutes to read • Edit Online

En algunos escenarios, como aplicaciones de página única (SPA), 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 una seguridad adicional.
ASP.NET Core 2.x
ASP.NET Core 1.x
Un esquema de autenticación se llama cuando el servicio de autenticación se configura 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: una para las cookies y otra de
portador.

NOTE
Especificar el esquema predeterminado de resultados en el HttpContext.User propiedad se establezca en esa identidad. Si
no se desea ese 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 va a 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 combinaciones que puede utilizar
independientemente de si se ha configurado 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 la oportunidad de crear y
agregar una identidad para el usuario actual. Al especificar 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 solo el controlador con el esquema de "Bearer". Se omiten las identidades basada
en cookies.

Seleccionar la combinación de directivas


Si desea especificar los esquemas deseados en directiva, puede establecer el AuthenticationSchemes al agregar la
directiva de colección:

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 del creado por el controlador
"Bearer". Usar la directiva estableciendo el [Authorize] del atributo Policy propiedad:

[Authorize(Policy = "Over18")]
public class RegistrationController : Controller

Usar varios esquemas de autenticación


Algunas aplicaciones que necesite admitir varios tipos de autenticación. Por ejemplo, la aplicación puede
autenticar a los usuarios de Azure Active Directory y de una base de datos de los usuarios. Otro ejemplo es una
aplicación que autentica a los usuarios de Active Directory Federation Services y Azure Active Directory B2C. En
este caso, la aplicación debe aceptar un token de portador JWT desde varios emisores.
Agregue todos los esquemas de autenticación que desea Aceptar. Por ejemplo, el siguiente código en
Startup.ConfigureServices agrega dos esquemas de autenticación de portador JWT con distintos emisores:
public void ConfigureServices(IServiceCollection services)
{
// Code omitted for brevity

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
})
.AddJwtBearer("AzureAD", options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://login.microsoftonline.com/eb971100-6f99-4bdc-8611-1bc8edd7f436/";
});
}

NOTE
Solo una autenticación de portador JWT se registra con el esquema de autenticación predeterminado
JwtBearerDefaults.AuthenticationScheme . Autenticación adicional que se tiene que estar registrada con un esquema de
autenticación único.

El siguiente paso es actualizar la directiva de autorización de forma predeterminada para aceptar ambos
esquemas de autenticación. Por ejemplo:

public void ConfigureServices(IServiceCollection services)


{
// Code omitted for brevity

services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"AzureAD");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
}

Como se invalida la directiva de autorización de forma predeterminada, es posible usar el [Authorize] atributo
en los controladores. El controlador, a continuación, acepta las solicitudes con el token JWT emitido por el emisor
de primer o segundo.
Protección de datos de ASP.NET Core
10/05/2019 • 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


La declaración del problema general puede establecerse de forma concisa 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
10/05/2019 • 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.
Información general sobre las API de consumidor
para ASP.NET Core
12/06/2019 • 7 minutes to read • Edit Online

El IDataProtectionProvider y IDataProtector interfaces son las interfaces básicas a través del cual los
consumidores usan 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. No puede utilizarse directamente
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. Consulte cadenas de propósito para mucha 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 es esta interfaz que los consumidores
pueden usar para realizar proteger y desproteger las operaciones.
Para proteger un elemento de datos, pase los datos a la Protect método. La interfaz básica define un método que
convierte byte -> byte [], pero hay también una sobrecarga (proporcionada como un método de extensión) que
convierte la cadena -> cadena. La seguridad de los dos métodos es idéntica; el desarrollador debe elegir cualquier
sobrecarga es más conveniente para su caso de uso. Con independencia de la sobrecarga elegida, el valor
devuelto por el proteja método ahora está protegido (cifrados y compatible con tecnologías de alteración) y la
aplicación puede enviar a un cliente de confianza.
Para desproteger un dato protegido anteriormente, pasar los datos protegidos en el Unprotect método. (Hay byte
[]-basados en cadena y basado en sobrecargas para mayor comodidad del desarrollador.) Si se genera la carga
protegida por una llamada anterior a Protect en este mismo IDataProtector , el Unprotect método devolverá la
carga sin protección original. Si la carga protegida ha sido alterada o fue creada por otro IDataProtector , el
Unprotect método producirá CryptographicException.

El concepto de la misma frente a diferentes IDataProtector ties realizar una copia en el concepto de propósito. Si
dos IDataProtector instancias se generaron a partir de la misma raíz IDataProtectionProvider pero a través de
las cadenas de propósito diferente en la llamada a IDataProtectionProvider.CreateProtector , a continuación, se
consideran diferentes protectores, y uno no podrá desproteger cargas de generan otros.

Consumo de estas interfaces


Para un componente compatible con DI, el uso previsto es que el componente toma una IDataProtectionProvider
parámetro en su constructor y que el sistema de DI ofrece este servicio automáticamente 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 compatibles con
DI por lo que no se puede usar el mecanismo descrito aquí. Para estos escenarios consulte la escenarios que no son
compatibles con DI documento para obtener más información sobre cómo obtener una instancia de un IDataProtection
proveedor sin tener que pasar a través de DI.

El ejemplo siguiente muestra tres conceptos:


1. Agregue el sistema de protección de datos al contenedor de servicios,
2. Uso de DI para recibir una instancia de un IDataProtectionProvider ,y
3. Creación de un IDataProtector desde un IDataProtectionProvider y usarlo 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 del desarrollador. Encapsula una única operación tanto
al 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
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.
Cadenas de propósito en ASP.NET Core
10/05/2019 • 6 minutes to read • Edit Online

Los componentes que consumen IDataProtectionProvider debe pasar un único fines parámetro para el
CreateProtector método. Los propósitos 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 propósito se usa junto con las claves criptográficas
de raíz para derivar subclaves criptográficas únicas a ese consumidor. Esto aísla el consumidor de todos los
consumidores criptográficos 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 procesa todas las categorías no es viable de
ataque contra el componente.

En el diagrama anterior, IDataProtector instancias A y B no leer de la otra las cargas, solo su propia.
La cadena de propósito no tiene que ser secreta. Simplemente debe ser único en el sentido de que ningún otro
componente con buen comportamiento sacarán nunca proporcionará la misma cadena de propósito.

TIP
Con el nombre de espacio de nombres y tipo del componente de 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 se encarga de los tokens de portador de minting podría usar
Contoso.Security.BearerToken como cadena de su propósito. O - incluso mejor - usaría Contoso.Security.BearerToken.v1
como cadena de su propósito. Anexa 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
cargas útiles de ir.

Desde el parámetro de fines CreateProtector es una matriz de cadenas, lo anterior se haya en su lugar,
especificado como [ "Contoso.Security.BearerToken", "v1" ] . Esto permite establecer una jerarquía de
propósitos y abre la posibilidad de escenarios de varios inquilinos con el sistema de protección de datos.
WARNING
Componentes no deben permitir la entrada del usuario de confianza a ser la única fuente 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 accidentalmente la mensajería segura sistema de mint
cargas que se podría 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" ]) , que proporciona aislamiento
adecuado.

El aislamiento que ofrecen los comportamientos y IDataProtectionProvider , IDataProtector , y con fines son los
siguientes:
Para un determinado IDataProtectionProvider objeto, el CreateProtector creará un IDataProtector
objeto vinculado de forma exclusiva a ambos el IDataProtectionProvider objeto que lo creó y el
parámetro de efectos que se pasó al método.
El parámetro propósito no debe ser null. (Si 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).
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 (utilizando a un
comparador ordinal) en el mismo orden. Un argumento único propósito es equivalente a la matriz de
fines único 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á


original unprotectedData si y solo si protectedData := Protect(unprotectedData) para equivalente
IDataProtector objeto.

NOTE
No estamos considerando el caso de que algún componente intencionadamente elige una cadena de propósito que se
sabe que entran en conflicto con otro componente. Este componente básicamente se consideraría malintencionado, y este
sistema no está diseñado para proporcionar garantías de seguridad en caso de que ya se está ejecutando código
malintencionado dentro del proceso de trabajo.
Jerarquía de propósito y la arquitectura multiempresa
en ASP.NET Core
10/05/2019 • 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
10/05/2019 • 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 cargas protegidas en ASP.NET
Core
10/05/2019 • 5 minutes to read • Edit Online

Hay escenarios donde el desarrollador de aplicaciones desea crear una carga protegida que expira después de 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 ciertamente posible para el desarrollador crear su propio
formato de carga que contiene una fecha de expiración incrustados, y los desarrolladores avanzados que desee
hacer esto de todos modos, pero para la mayoría de los desarrolladores administrar la caducidad de estas puede
crecer tedioso.
Para facilitar esta tarea para nuestra audiencia de desarrolladores, el paquete
Microsoft.AspNetCore.DataProtection.Extensions contiene API de la utilidad para crear cargas que 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 las cargas de tiempo limitado /
expiración automática. Para crear una instancia de un ITimeLimitedDataProtector , primero tendrá que una instancia
de 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 obtener un protector
de vuelta con capacidades integradas de expiración.
ITimeLimitedDataProtector expone los métodos de extensión y de superficie de API siguientes:
CreateProtector (fin de cadena): ITimeLimitedDataProtector - esta API es similar a la existente
IDataProtectionProvider.CreateProtector en que se puede usar para crear finalidad cadenas desde un
protector de tiempo limitado de raíz.
Proteger (byte [] texto simple, expiración 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 DateTimeOffset): cadena
Proteger (texto simple de cadena, duración del intervalo de tiempo): cadena
Proteger (texto simple de cadena): cadena
Además de los principales Protect métodos que admiten solo el texto sin formato, 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 tiempo a través de un
TimeSpan ). Si se llama a una sobrecarga que no tiene una fecha de expiración, la carga se supone que nunca
expiran.
Desproteger (byte [] protectedData, out DateTimeOffset expiración): byte]
Desproteger (byte [] protectedData): byte]
Desproteger (cadena protectedData, out DateTimeOffset expiración): cadena
Desproteger (cadena protectedData): cadena
El Unprotect métodos devuelven los datos originales sin protección. Si aún no ha expirado la carga, la expiración
absoluta se devuelve como un parámetro junto con los datos originales sin protección de salida opcionales. Si la
carga ha caducado, todas las sobrecargas del método Unprotect producirá CryptographicException.

WARNING
No se recomienda utilizar estas API para proteger las cargas que requieren la persistencia a largo plazo o indefinida. "¿Puedo
permitirme para que las cargas protegidas que recuperarse después de un mes?" puede actuar como una buena regla
general; Si la respuesta es no a los desarrolladores a continuación, considere la API alternativas.

El ejemplo siguiente se usa el las rutas de código que no sean de DI para instanciar el sistema de protección de
datos. Para ejecutar este ejemplo, asegúrese de que primero ha agregado una referencia al paquete
Microsoft.AspNetCore.DataProtection.Extensions.

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

*/
Desprotección de cargas cuyas claves se han
revocado en ASP.NET Core
10/05/2019 • 5 minutes to read • Edit Online

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. Las claves no se quitan nunca de
conjunto de claves, por lo que IDataProtector.Unprotect siempre se puede recuperar las cargas existentes siempre
que las claves están disponibles y son válidos.
Sin embargo, un problema se produce cuando el desarrollador 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
interesante cargas breves o temporales (por ejemplo, tokens de autenticación), que fácilmente se pueden volver a
crear estos tipos de cargas por el sistema y, en el peor caso el visitante del sitio podría ser necesario volver a iniciar
sesión. Pero para las cargas persistentes, tener Unprotect throw podría provocar la pérdida de datos inaceptable.

IPersistedDataProtector
Para admitir el escenario de permitir que las cargas que se debe desproteger aunque se produzcan revocadas
claves, el sistema de protección de datos contiene un IPersistedDataProtector tipo. Para obtener una instancia de
IPersistedDataProtector , obtener simplemente una instancia de IDataProtector en la forma normal y la
conversión de try el IDataProtector a IPersistedDataProtector .

NOTE
No todos los IDataProtector instancias se pueden convertir en IPersistedDataProtector . Los desarrolladores deben
usar la C# como operador o similar evitar las excepciones en tiempo de ejecución causados por las conversiones no válidas, y
deben estar preparados para controlar el caso de error 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 no protegida. 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 activa
de forma predeterminada, por ejemplo, la clave utilizada para proteger esta carga es antigua y tiene una
clave de revertir la operación desde tendrán lugar. El llamador que desee considerar volver a proteger la
carga según 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 se ha revocado
y autenticidad de la carga se debe tratar como sospechosa. En este caso, sólo seguir funcionando en la carga no protegida si
tiene cierta seguridad independiente que es auténtico, por ejemplo, que procede de una base de datos seguro en lugar de
que se envían por un cliente web 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
10/05/2019 • 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
13/06/2019 • 19 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 Blob Storage, solo la aplicación web debe tener la capacidad de leer, escribir o
crear nuevas entradas en el almacén de blobs, 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 Storage. 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, una clave creada en el almacén de claves denominado dataprotection en el contosokeyvault
tiene el identificador de clave https://contosokeyvault.vault.azure.net/keys/dataprotection/ . Proporcione
la aplicación con Unwrap Key y Wrap Key permisos para el almacén de claves.
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 permite aislar las aplicaciones entre sí
basándose en sus rutas de acceso raíz del contenido, 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 protegido cargas entre aplicaciones:
Configurar SetApplicationName en cada aplicación con el mismo valor.
Utilice la misma versión de la pila de la API de protección de datos a través de las aplicaciones. Realizar
cualquier de las siguientes acciones en archivos de proyecto de las aplicaciones:
Hacer referencia a la misma versión del marco compartido a través de la
Microsoft.AspNetCore.App metapaquete.
Hace referencia al mismo paquete de protección de datos versió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 es
la ruta de acceso física de la aplicación:
Para las aplicaciones hospedadas en IIS, el identificador único es la ruta de acceso física de IIS de la
aplicación. Si una aplicación se implementa en un entorno de granja de servidores web, este valor es
estable, suponiendo que los entornos de IIS se configuran de forma similar en todos los equipos en la
granja de servidores web.
Para aplicaciones autohospedadas que se ejecutan el servidor Kestrel, el identificador único es la ruta de
acceso física a la aplicación en el disco.
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:
services.AddDataProtection()
.UseCryptographicAlgorithms(
new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});

services.AddDataProtection()
.UseCryptographicAlgorithms(
new AuthenticatedEncryptionSettings()
{
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


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

Para especificar algoritmos administrados personalizados, cree un


ManagedAuthenticatedEncryptionSettings instancia que apunta a los tipos de implementación:
serviceCollection.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new ManagedAuthenticatedEncryptionSettings()
{
// 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


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

Para especificar un algoritmo personalizado de CNG de Windows con cifrado CBC -modo de validación de
HMAC, cree un CngCbcAuthenticatedEncryptionSettings instancia que contiene la información algorítmica:
services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngCbcAuthenticatedEncryptionSettings()
{
// 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.

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

Para especificar un algoritmo CNG de Windows personalizado mediante el cifrado de modo contador de
Galois/con la validación, cree un CngGcmAuthenticatedEncryptionSettings instancia que contiene la
información algorítmica:

services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngGcmAuthenticatedEncryptionSettings()
{
// 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.

Recursos adicionales
Escenarios compatibles con DI no para la protección de datos en ASP.NET Core
Directiva de todo el equipo de protección de datos se admiten en ASP.NET Core
Hospedaje de ASP.NET Core en una granja de servidores web
Proveedores de almacenamiento de claves en ASP.NET Core
Administración de claves de protección de datos y la
duración en ASP.NET Core
10/05/2019 • 6 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.
También se debe habilitar el atributo setProfileEnvironment del grupo de aplicaciones. El valor
predeterminado de setProfileEnvironment es true . En algunos escenarios (por ejemplo, SO Windows),
setProfileEnvironment está establecido en false . Si las claves no se almacenan en el directorio del perfil
de usuario como se esperaba:
a. Vaya a la carpeta %windir%/system32/inetsrv/config.
b. Abra el archivo applicationHost.config.
c. Busque el elemento
<system.applicationHost><applicationPools><applicationPoolDefaults><processModel> .
d. Confirme que el atributo setProfileEnvironment no está presente, que adopta de forma
predeterminada el valor true , o establezca explícitamente el valor del atributo en true .
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
Directiva de todo el equipo de protección de datos
se admiten en ASP.NET Core
10/05/2019 • 7 minutes to read • Edit Online

Por Rick Anderson


Cuando se ejecuta en Windows, el sistema de protección de datos con compatibilidad limitada para establecer
una directiva de todo el equipo de forma predeterminada para todas las aplicaciones que consumen la protección
de datos de ASP.NET Core. La idea general es que un administrador que desee cambiar una configuración
predeterminada, 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 la
aplicación siempre puede reemplazar cualquier valor con uno de su propia elección. La directiva predeterminada solo afecta
a las aplicaciones donde el desarrollador no ha especificado un valor explícito para una configuración.

Establecer la directiva predeterminada


Para establecer la directiva 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 afectar el comportamiento de las aplicaciones de 32 bits, no
olvide configurar el equivalente de Wow6432Node de la clave anterior.
A continuación, se muestran los valores admitidos.

VALOR TIPO DESCRIPCIÓN

EncryptionType string Especifica los algoritmos que se deben


usar para la protección de datos. El
valor debe ser CNG-CBC, CNG GCM o
administrado y se describe más
detalladamente a continuación.

DefaultKeyLifetime DWORD Especifica la duración de las claves


recién generadas. El valor se especifica
en días y debe ser > = 7.

KeyEscrowSinks string 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 en la lista es el nombre
completo de ensamblado de un tipo
que implementa IKeyEscrowSink.

Tipos de cifrado
Si EncryptionType es CNG -CBC, el sistema está configurado para usar un cifrado por bloques simétrico modo
CBC para confidencialidad y HMAC de autenticidad con servicios proporcionados por Windows CNG (consulte
especificando algoritmos personalizados de Windows CNG para más detalles). Se admiten los siguientes valores
adicionales, cada uno de los cuales corresponde a una propiedad del tipo
CngCbcAuthenticatedEncryptionSettings.

VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithm string El nombre de un algoritmo de cifrado


de bloques simétricos entendido CNG.
Este algoritmo se abre en modo CBC.

EncryptionAlgorithmProvider string El nombre de la implementación del


proveedor CNG que puede producir el
algoritmo de cifrado del algoritmo.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para la


derivación para el algoritmo de cifrado
de bloques simétricos.

HashAlgorithm string El nombre de un algoritmo hash


entendido CNG. Este algoritmo se abre
en modo HMAC.

HashAlgorithmProvider string El nombre de la implementación del


proveedor CNG que puede producir el
algoritmo de 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
(consulte especificando algoritmos personalizados de Windows CNG Para obtener más detalles). Se admiten los
siguientes valores adicionales, cada uno de los cuales corresponde a una propiedad del tipo
CngGcmAuthenticatedEncryptionSettings.

VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithm string El nombre de un algoritmo de cifrado


de bloques simétricos entendido CNG.
Este algoritmo se abre en modo de
Galois o contador.

EncryptionAlgorithmProvider string El nombre de la implementación del


proveedor CNG que puede producir el
algoritmo de cifrado del algoritmo.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para la


derivación para el algoritmo de cifrado
de bloques simétricos.

Si está administrado EncryptionType, el sistema está configurado para usar un SymmetricAlgorithm


administrado para la confidencialidad y KeyedHashAlgorithm para autenticidad (consulte especificar
personalizado administrado algoritmos para obtener más detalles). Se admiten los siguientes valores adicionales,
cada uno de los cuales corresponde a una propiedad del tipo ManagedAuthenticatedEncryptionSettings.
VALOR TIPO DESCRIPCIÓN

EncryptionAlgorithmType string El nombre completo de ensamblado de


un tipo que implementa
SymmetricAlgorithm.

EncryptionAlgorithmKeySize DWORD La longitud (en bits) de la clave para


derivar el algoritmo de cifrado
simétrico.

ValidationAlgorithmType string El nombre completo de 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 incluye los nombres de tipo (EncryptionAlgorithmType,
ValidationAlgorithmType, KeyEscrowSinks), los tipos deben ser disponibles para la aplicación. Esto significa que para las
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 las aplicaciones de ASP.NET Core que se ejecutan en .NET Core, se deben instalar
los paquetes que contienen estos tipos.
Escenarios compatibles con DI no para la protección
de datos en ASP.NET Core
17/06/2019 • 4 minutes to read • Edit Online

Por Rick Anderson


El sistema de protección de datos de ASP.NET Core es normalmente agregado a un contenedor de servicios y
consumo de los componentes dependientes a través de la inserción de dependencias (DI). Sin embargo, hay
casos donde esto no es viable o deseado, 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 protección de datos sin tener que
depender de 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, el DataProtectionProvider tipo concreto no cifra el material de clave sin procesar
guardarlos antes en el sistema de archivos. Esto es para admitir escenarios donde los puntos de desarrollador a
un recurso compartido de red y el sistema de protección de datos no pueden deducir automáticamente un
mecanismo de cifrado de claves en reposo adecuado.
Además, el DataProtectionProvider tipo concreto no aislar aplicaciones de forma predeterminada. Todas las
aplicaciones con el mismo directorio clave pueden compartir las cargas siempre y cuando sus finalidad
parámetros coinciden.
El DataProtectionProvider constructor acepta una devolución de llamada de configuración opcional que puede
usarse para ajustar los comportamientos del sistema. El ejemplo siguiente muestra la restauración aislamiento
con una llamada explícita a SetApplicationName. El ejemplo también muestra la configuración del sistema para
cifrar automáticamente claves persistentes mediante DPAPI de Windows. Si el directorio señala a un recurso
compartido UNC, es posible que desee distribuir un certificado compartido entre todos los equipos
correspondientes y configurar el sistema para usar 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
Las instancias de la DataProtectionProvider son caros de crear un tipo concreto. Si una aplicación mantiene varias
instancias de este tipo y todos usan el mismo directorio de almacenamiento de claves, podría 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 creadas a partir de instancias son seguras para
subprocesos para varios de los llamadores.
API de extensibilidad de protección de datos de
ASP.NET Core
10/05/2019 • 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 en ASP.NET
Core
10/05/2019 • 11 minutes to read • Edit Online

WARNING
Tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para varios de los
llamadores.

IAuthenticatedEncryptor
El IAuthenticatedEncryptor interfaz es el bloque de creación básico del subsistema criptográfico. Generalmente
es una IAuthenticatedEncryptor por clave, y la instancia IAuthenticatedEncryptor encapsula toda la información
algorítmica necesaria para realizar operaciones criptográficas y material clave criptográfico.
Como sugiere su nombre, el tipo es responsable de proporcionar servicios de cifrado y descifrado autenticados.
Expone las siguientes dos API.
Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData) : byte[]

Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData) : byte[]

El método Encrypt devuelve un blob que incluye el texto no cifrado cifrado y una etiqueta de autenticación. La
etiqueta de autenticación debe incluir los datos adicionales de autenticado (AAD ), aunque el propio AAD no se
debe recuperar de la carga final. El método Decrypt valida la etiqueta a la autenticación y devuelve la carga
deciphered. Todos los errores (excepto ArgumentNullException y similares) deben homogeneizarse a
CryptographicException.

NOTE
La propia instancia IAuthenticatedEncryptor realmente no tiene que 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. Su API es como sigue.
CreateEncryptorInstance (clave IKey): IAuthenticatedEncryptor
Para cualquier instancia dada de IKey, cualquier rechazarán autenticado creados por su método
CreateEncryptorInstance debe considerarse equivalente, 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 2.x solo)


ASP.NET Core 2.x
ASP.NET Core 1.x
El IAuthenticatedEncryptorDescriptor interfaz representa un tipo que sabe cómo exportar a XML. Su 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 suministrarle los argumentos válidos. Considere la posibilidad de un
IAuthenticatedEncryptor cuya implementación se basa en SymmetricAlgorithm y KeyedHashAlgorithm. Trabajo
del sistema de cifrado es consumen estos tipos, pero no sabe 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 actúa como un nivel más alto por encima 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 ese conocimiento en
formato XML para que la instancia de sistema de cifrado se puede volver a crear después una aplicación de
restablecimiento.
El descriptor se puede serializar a través de su rutina 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 resucitar este
descriptor dada la XElement correspondiente.
El descriptor serializado puede contener información confidencial, como material de clave criptográfica. El sistema
de protección de datos tiene compatibilidad integrada para cifrar la información antes de que se conserva en el
almacenamiento. Para aprovechar las ventajas de esto, el descriptor debe 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 el
caso de una clave criptográfica que se almacenan en un HSM de nuevo. El descriptor no se puede escribir el
material de clave al serializar propio dado que el HSM no expone el material en texto sin formato. En su lugar,
puede escribir el descriptor de la versión de encapsulado de clave 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 de 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
El elemento 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
Piense en AlgorithmConfiguration como el generador de nivel superior. La configuración actúa como una plantilla.
Encapsula información algorítmico (p. ej., esta configuración produce descriptores con una clave maestra de AES -
128-GCM ), pero aún no está asociada con una clave específica.
Cuando se llama a CreateNewDescriptor, nuevo material de clave 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 mantiene en memoria),
podría crearse y contenido en un HSM y así sucesivamente. El punto fundamental es que las dos llamadas a
CreateNewDescriptor nunca deben crear instancias de IAuthenticatedEncryptorDescriptor equivalente.
El tipo AlgorithmConfiguration actúa como punto de entrada para las rutinas de creación de claves como clave
automatic gradual. Para cambiar la implementación para todas las claves de futuras, establezca la propiedad
AuthenticatedEncryptorConfiguration en KeyManagementOptions.
Extensibilidad de administración de claves en
ASP.NET Core
10/05/2019 • 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
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:
Encrypt(XElement plaintextElement): 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 (XElement encryptedElement): 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;
using Microsoft.Extensions.Logging;

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,
new LoggerFactory());
}

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
10/05/2019 • 2 minutes to read • Edit Online

WARNING
Tipos que implementan cualquiera de las interfaces siguientes deben ser seguro para subprocesos para varios de los
llamadores.

ISecret
El ISecret interfaz representa un valor de secreto, como material de clave criptográfica. 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 del secreto sin procesar. El motivo de
esta API toma el búfer como un parámetro en lugar de devolver un byte[] directamente es que esto proporciona
el llamador la oportunidad para anclar el objeto de búfer, limitar la exposición secreta para el recolector de
elementos no utilizados administrado.
El Secret tipo es una implementación concreta de ISecret donde el valor del secreto se almacena en memoria
en proceso. En las plataformas Windows, el valor del secreto se cifra mediante CryptProtectMemory.
Implementación de protección de datos de ASP.NET
Core
10/05/2019 • 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
10/05/2019 • 4 minutes to read • Edit Online

Las llamadas a IDataProtector.Protect son las operaciones de cifrado autenticado. El método Protect ofrece
confidencialidad y la autenticidad y está vinculado a la cadena de propósito que se usó para derivar esta instancia
concreta de IDataProtector 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 protegida de la cadena. Si se usa esta API todavía
tendrá el formato de carga protegido el por debajo de la estructura, pero será codificados 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, la clave que representa un AES -256-CBC + cifrador HMACSHA256 y la carga se
subdivide además como sigue:
Un modificador de la clave de 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 protegida 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 que 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 en una clave determinada se iniciará con el mismo encabezado de 20 bytes (valor mágico, Id. de
clave). Los administradores pueden usar este hecho para fines de diagnóstico para aproximarse a cuando se generó 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 modificado) se ha generado dentro de esa
ventana, conceda a o tomar una pequeña factor aglutinante a ambos lados.
Derivación de subclave y cifrado autenticado en
ASP.NET Core
10/05/2019 • 8 minutes to read • Edit Online

La mayoría de teclas en el conjunto de claves contendrá alguna forma de entropía y tendrá algorítmico
información que indique "el cifrado de modo CBC + validación HMAC" o "GCM cifrado y validación". En estos
casos, nos referimos a la entropía incrustada como el material de claves principal (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 no es posible que se comportan como sigue. Si la clave
proporciona su propia implementación de IAuthenticatedEncryptor en lugar de usar una de nuestras fábricas integradas,
el mecanismo descrito en esta sección ya no es aplicable.

Derivación de subclave y de datos autenticados adicionales


El IAuthenticatedEncryptor interfaz actúa como la interfaz principal 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 es generada por el
sistema AAD y consta de tres componentes:
1. El encabezado 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 tiene el formato de la cadena de propósito que creó el IDataProtector que
está realizando esta operación.
Dado que es única para la tupla de los tres componentes AAD, se puede usar para derivar nuevas claves de KM en
lugar de usar KM de sí mismo en todos los de nuestras operaciones criptográficas. Para cada llamada a
IAuthenticatedEncryptor.Encrypt , tiene lugar el proceso de derivación de claves siguiente:

( K_E, K_H ) = SP800_108_CTR_HMACSHA512(K_M, AAD, contextHeader || keyModifier)


En este caso, estamos llamando a NIST SP800-108 KDF en el modo contador (consulte NIST SP800-108, s. 5.1)
con los siguientes parámetros:
Clave de derivación de claves (KDK) = K_M
PRF = HMACSHA512
etiqueta = additionalAuthenticatedData
context = 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 derivan K_E y K_H. El modificador de tecla es una cadena de 128 bits generada
aleatoriamente para cada llamada a Encrypt y sirve para asegurarse de sobrecargar la probabilidad de que sean
únicos para esta operación de cifrado de autenticación específico, KE y KH incluso si todas las demás entradas a
KDF es constante.
Para el cifrado de modo CBC + las operaciones de validación de HMAC, | K_E | es la longitud de la clave de cifrado
de bloques simétricos y | K_H | es el tamaño de la síntesis de la rutina HMAC. Para el cifrado de GCM y las
operaciones de validación, | K_H | = 0.

Cifrado de modo CBC + validación HMAC


Una vez que K_E se genera mediante el mecanismo anterior, se genera un vector de inicialización aleatorio y se
ejecuta el algoritmo de cifrado de bloques simétrico para cifrar el texto no cifrado. 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ágico y el Id. de clave a salida antes de
devolverla al llamador. Dado que el encabezado mágico 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 carga final devuelta es
autenticado por el equipo Mac.

El cifrado de modo contador/Galois + validación


Una vez que K_E se genera mediante el mecanismo anterior, se genere un nonce de 96 bits aleatorio y se ejecuta el
algoritmo de cifrado de bloques simétrico para cifrar el texto no cifrado y generar la etiqueta a la autenticación de
128 bits.

salida: = keyModifier || valor nonce || E_gcm (K_E, nonce, los datos) || authTag
NOTE
Aunque GCM admite de forma nativa el concepto de AAD, nos estamos aún la alimentación de AAD solo KDF original, para
pasar una cadena vacía a GCM para su parámetro AAD. La razón de esto es doble. Primero, para admitir la agilidad nunca
deseamos 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 sea invocado nunca 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 corrige K_E nos no podemos realizar más
de 2 ^ 32 operaciones de cifrado antes de ejecutar que realizamos mantiene de las 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. Para estar al día de 2 ^ límite probabilidad-32, seguiremos
usando un modificador de clave de 128 bits y 96 bits nonce, que amplía radicalmente el número de operaciones pueden usar
para cualquier K_M determinado. Por motivos de simplicidad del diseño que compartimos la ruta de acceso del código KDF
entre las operaciones 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
10/05/2019 • 16 minutes to read • Edit Online

En 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 se identifica mediante un identificador único (GUID ) y lo lleva información
algorítmica y material entropic. Se pretende que llevar a cada clave única entropía, pero no puede aplicar el
sistema, y también es necesario tener en cuenta los desarrolladores que podrían cambiar el conjunto de claves
manualmente mediante la modificación de la información de una clave existente en el conjunto de claves
algorítmica. Para lograr los requisitos de seguridad dados en estos casos, el sistema de protección de datos tiene
un concepto de agilidad criptográfica, lo que permite de forma segura con un único valor entropic entre varios
algoritmos criptográficos.
La 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 dentro de la carga. OID del algoritmo suele ser un buen candidato
para esto. Sin embargo, un problema que nos enfrentamos es que hay varias maneras de especificar el mismo
algoritmo: "AES" (CNG ) y el administrado Aes, AesManaged, AesCryptoServiceProvider, AesCng y
RijndaelManaged (determinados parámetros específicos) las clases son realmente todo lo mismo, y sería necesario
mantener una asignación de todos ellos al OID correcto. Si un desarrollador desea proporcionar un algoritmo
personalizado (o incluso otra implementación de AES ), tendría para indicarnos su OID. Este paso de registro
adicional hace que la configuración del sistema que especialmente complicado.
Volviendo atrás, decidimos que nos estábamos abordar el problema de la dirección equivocada. Un OID indica
cuál es el algoritmo, pero no nos realmente interesa esto. Si es necesario usar un único valor entropic de forma
segura en dos diferentes algoritmos, no es necesario para que podamos saber cuáles son realmente los algoritmos.
Lo que realmente importa es que su comportamiento. Cualquier algoritmo de cifrado de bloques simétrico
decente también es una permutación pseudoaleatorio segura (PRP ): corrija las entradas (clave, texto simple, IV, el
modo de encadenamiento) y la salida de texto cifrado con una sobrecarga de probabilidad sea distinta de cualquier
otro cifrado por bloques simétrico algoritmo dada las entradas de la mismas. De forma similar, cualquier función
de hash con clave decente también es una función pseudoaleatorio segura (PRF ) y, dado un conjunto fijo de
entrada su salida inmensa mayoría será distinta de cualquier otra función de hash con clave.
Este concepto de seguro PRPs y PRFs se usa para crear un encabezado de contexto. Este encabezado de contexto
actúa esencialmente como una huella digital del estable a través de los algoritmos en uso para una operación
determinada y proporciona 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 subclave. Hay
dos maneras de crear el encabezado de contexto dependiendo de los modos de funcionamiento de los algoritmos
subyacentes.

Cifrado de modo CBC + autenticación HMAC


El encabezado de contexto se compone de 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étricos.
[32 bits] El tamaño de bloque (en bytes, big-endian) del algoritmo de cifrado de bloques simétricos.
[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 la síntesis (en bytes, big-endian) del algoritmo HMAC.
EncCBC (K_E, IV, ""), que es el resultado del algoritmo de cifrado de bloques simétrico según una entrada de
cadena vacía y donde el vector de inicialización es un vector de ceros. A continuación se describe la
construcción de K_E.
MAC (K_H, ""), que es el resultado del algoritmo HMAC según una entrada de cadena vacía. A continuación
se describe la construcción de K_H.
Idealmente, podemos pasar vectores ceros para K_E y K_H. Sin embargo, queremos 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 ), que impide utilizando un modelo repetible o simple como un vector de ceros.
En su lugar, usamos NIST SP800-108 KDF en el modo contador (consulte 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 a sí mismos. Matemáticamente, esto se
representa como sigue.
( K_E || K_H ) = SP800_108_CTR (prf = HMACSHA512, key = "", label = "", context = "")
Ejemplo: AES -192-CBC + HMACSHA256
Por ejemplo, considere el caso donde el algoritmo de cifrado de bloques simétrico es AES -CBC -192 y el algoritmo
de validación es HMACSHA256. El sistema generaría el encabezado de contexto mediante los pasos siguientes.
First, let ( K_E || K_H ) = SP800_108_CTR (prf = HMACSHA512, key = "", label = "", context = ""), where | K_E | =
192 bits and | K_H | = 256 bits per the specified algorithms. Esto conduce a 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 anterior.
result := F474B1872B3B53E4721DE19C0841DB6F
A continuación, calcular MAC (K_H, "") para HMACSHA256 dado K_H anterior.
resultado: = D4791184B996092EE1202F36E8608FA8FBD98ABDFF5402F264B1D7211536220C
Esto genera el encabezado de contexto 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 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 de bloques salida PRP (74 F4 - DB 6F ) y
la salida de PRF de HMAC (D4 79 - final).

NOTE
El cifrado de modo CBC + HMAC encabezado de contexto de autenticación se basa en la misma manera independientemente
de si se proporcionan las implementaciones de algoritmos CNG de Windows o los tipos administrados SymmetricAlgorithm y
KeyedHashAlgorithm. Esto permite que las aplicaciones que se ejecutan en diferentes sistemas operativos producir el mismo
encabezado de contexto de forma confiable, aunque las implementaciones de los algoritmos se diferencian entre los sistemas
operativos. (En la práctica, la KeyedHashAlgorithm no tiene que ser un HMAC adecuado. Puede ser cualquier tipo de
algoritmo hash con clave.)

Ejemplo: 3DES -192-CBC + HMACSHA1


First, let ( K_E || K_H ) = SP800_108_CTR (prf = HMACSHA512, key = "", label = "", context = ""), where | K_E | =
192 bits and | K_H | = 160 bits per the specified algorithms. Esto conduce a 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 anterior.
resultado: = ABB100F81E53E10E
A continuación, calcular MAC (K_H, "") para HMACSHA1 dado K_H anterior.
result := 76EB189B35CF03461DDF877CD9F4B1B4D63A7555
Esto genera el encabezado de contexto 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 de bloques salida PRP (B1 AB - E1 0E ) y
la salida de PRF de HMAC (76 EB - final).

El cifrado de modo contador/Galois + autenticación


El encabezado de contexto se compone de 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étricos.
[32 bits] El tamaño (en bytes, big-endian) nonce que usa durante las operaciones de cifrado autenticado.
(Para nuestro sistema, esto se ha corregido en tamaño nonce = 96 bits.)
[32 bits] El tamaño de bloque (en bytes, big-endian) del algoritmo de cifrado de bloques simétricos. (Para
GCM, esto se fija en el tamaño de bloque = 128 bits.)
[32 bits] La autenticación tamaño de etiqueta (en bytes, big-endian) creado por la función de cifrado
autenticado. (Para nuestro sistema, esto se fija en el tamaño de 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 según 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 se muestra en el cifrado CBC + el escenario de autenticación de
HMAC. Sin embargo, puesto que no hay ningún K_H en juego aquí, básicamente, tenemos | K_H | = 0, y el
algoritmo se contrae en el siguiente formulario.
K_E = SP800_108_CTR (prf = HMACSHA512, key = "", label = "", context = "")
Ejemplo: AES -256-GCM
First, let K_E = SP800_108_CTR (prf = HMACSHA512, key = "", label = "", context = ""), where | K_E | = 256 bits.
K_E := 22BC6F1B171C08C4AE2F27444AF8FC8B3087A90006CAEA91FDCFB47C1B8733B8
A continuación, calcular la etiqueta a la autenticación de Enc_GCM (K_E, nonce, "") de AES -256-GCM dado nonce
= 096 y K_E anterior.
resultado: = E7DCCE66DF855A323A6BB7BD7A59BE45
Esto genera el encabezado de contexto 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 de nonce (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 a la autenticación de la ejecución el cifrado de bloques (controlador de dominio E7 - final).
Administración de claves en ASP.NET Core
10/05/2019 • 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
18/06/2019 • 7 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.

Almacenamiento de Azure
El Microsoft.AspNetCore.DataProtection.AzureStorage paquete permite almacenar las claves de protección de
datos en Azure Blob Storage. 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>"));
}

Si la aplicación web se ejecuta como un servicio de Azure, los tokens de autenticación pueden crearse
automáticamente con Microsoft.Azure.Services.AppAuthentication.
var tokenProvider = new AzureServiceTokenProvider();
var token = await tokenProvider.GetAccessTokenAsync("https://storage.azure.com/");
var credentials = new StorageCredentials(new TokenCredential(token));
var storageAccount = new CloudStorageAccount(credentials, "mystorageaccount", "core.windows.net", useHttps:
true);
var client = storageAccount.CreateCloudBlobClient();
var container = client.GetContainerReference("my-key-container");

// optional - provision the container automatically


await container.CreateIfNotExistsAsync();

services.AddDataProtection()
.PersistKeysToAzureBlobStorage(container, "keys.xml");

Vea más detalles acerca de cómo configurar la autenticación de servicio a servicio.

Redis
El Microsoft.AspNetCore.DataProtection.StackExchangeRedis paquete permite almacenar las claves de
protección de datos 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.
El Microsoft.AspNetCore.DataProtection.Redis paquete permite almacenar las claves de protección de datos 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 Redis, llame a uno de los PersistKeysToStackExchangeRedis sobrecargas:

public void ConfigureServices(IServiceCollection services)


{
var redis = ConnectionMultiplexer.Connect("<URI>");
services.AddDataProtection()
.PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");
}

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 e implementar 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<MyKeysContext> options)
: base(options) { }

// This maps to the table that stores keys.


public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
}

Crear el DataProtectionKeys tabla.


Visual Studio
CLI de .NET Core
Ejecute los comandos siguientes en el Package Manager Console ventana (PMC ):

Add-Migration AddDataProtectionKeys -Context MyKeysContext


Update-Database -Context MyKeysContext

MyKeysContext es el DbContext definido en el ejemplo de código anterior. Si usas un DbContext con un


nombre diferente, sustituya su DbContext nombre MyKeysContext .
La DataProtectionKeys clase/entidad adopta la estructura que se muestra en la tabla siguiente.

PROPIEDAD O CAMPO. TIPO CLR TIPO DE SQL

Id int int , PK, no es null

FriendlyName string nvarchar(MAX) , null

Xml string nvarchar(MAX) , null

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
10/05/2019 • 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
10/05/2019 • 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
10/05/2019 • 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 <encryptedSecret> 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ímeros en
ASP.NET Core
10/05/2019 • 2 minutes to read • Edit Online

Hay escenarios donde una aplicación necesita un throwaway IDataProtectionProvider . Por ejemplo, el
desarrollador simplemente se podría experimentar una aplicación de consola única 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
únicamente en memoria y no se escriben en ningún almacén de respaldo.
Cada instancia de EphemeralDataProtectionProvider usa su propia clave principal único. Por lo tanto, si un
IDataProtector cuya raíz comienza en un EphemeralDataProtectionProvider genera una carga protegida, esa carga
sólo puede desproteger un equivalente IDataProtector (recibe la misma propósito cadena) cuya raíz comienza en
el mismo EphemeralDataProtectionProvider instancia de.
El siguiente ejemplo muestra cómo crear una instancia de un EphemeralDataProtectionProvider y usarlo 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
10/05/2019 • 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
10/05/2019 • 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 enrute 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 ser instalado en una aplicación existente de ASP.NET destinadas a .NET
4.5.1 o posterior. Se producirá un error si la aplicación tiene como destino .NET 4.5 instalación o se reduzca.

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 el configuración predeterminada configuración.
Cuando se instala el paquete, inserta una línea en Web.config que le indica a ASP.NET que se usa para más
operaciones criptográficas, incluida la autenticación de formularios, el estado de vista y las 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 los campos como
__VIEWSTATE , que debe comenzar por "CfDJ8" como se muestra en el ejemplo siguiente. "CfDJ8" es la representación en
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 paquete
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, dado que de forma predeterminada las claves se conservan en el 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 cuyas subclases
DataProtectionStartup e invalida su método ConfigureServices.
A continuación es un ejemplo de un tipo de inicio de protección de datos personalizados que había configurado
donde se almacenan las claves y cómo se cifran en reposo. También invalida la directiva de aislamiento de
aplicaciones de forma predeterminada, ya que proporciona 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 conveniencia para evitar forzar al desarrollador a crear un tipo derivado
de DataProtectionStartup si todos los que desean configurar se estableciendo el nombre de la aplicación.

Para habilitar esta configuración personalizada, vuelva al archivo Web.config y busque el <appSettings> elemento
que instalar el paquete agregado al archivo de configuración. Tendrá un aspecto como el marcado siguiente:

<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 configuradas ahora está listo para su uso dentro de la aplicación.
Almacenamiento seguro de secretos de
aplicación en el desarrollo en ASP.NET Core
10/05/2019 • 18 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.

Al trabajar con claves jerárquicas en variables de entorno, es posible que un separador de dos puntos
( : ) no funcione en todas las plataformas (por ejemplo, Bash). Un guion bajo doble ( __ ) es
compatible con todas las plataformas y lo reemplaza un separador de dos puntos.

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

Habilitar el almacenamiento de secretos


La herramienta Secret Manager opera en valores de configuración de específicas del proyecto
almacenados en su perfil de usuario.
La herramienta Secret Manager incluye una init comando 3.0.100 del SDK de .NET Core o
posterior. Para usar secretos de usuario, ejecute el siguiente comando en el directorio del proyecto:

dotnet user-secrets init

El comando anterior agrega una UserSecretsId elemento dentro de un PropertyGroup de la .csproj


archivo. De forma predeterminada, el texto interno de UserSecretsId es un GUID. El texto interno es
arbitrario, pero es único para el proyecto.
Para usar secretos de usuario, defina un UserSecretsId elemento dentro de un PropertyGroup de la
.csproj archivo. El texto interno 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.

Establezca un secreto
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"

Estructura JSON acoplado en Visual Studio


Visual Studio administrar secretos de usuario gestos se 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"
}

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
Linux / macOS
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 Development:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();

Cuando CreateDefaultBuilder no llama, agregar el origen de configuración de los secretos de usuario


de forma explícita mediante una llamada a AddUserSecrets en el Startup constructor. Llamar a
AddUserSecrets solo cuando la aplicación se ejecuta en el entorno de desarrollo, como se muestra en
el ejemplo siguiente:

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

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 Password


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)

Вам также может понравиться