Vous êtes sur la page 1sur 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."