Tutorial: Control de simultaneidad: ASP.NET MVC conEF Core

En los tutoriales anteriores, aprendió a actualizar los datos. Este tutorial muestra cómo tratar los conflictos cuando varios usuarios actualizan la misma entidad al mismo tiempo.

Podrá crear páginas web que funcionan con la entidad Department y controlan los errores de simultaneidad. Las siguientes ilustraciones muestran las páginas Edit y Delete, incluidos algunos mensajes que se muestran si se produce un conflicto de simultaneidad.

Department Edit page

Department Delete page

En este tutorial ha:

  • Obtiene información sobre los conflictos de simultaneidad
  • Agrega una propiedad de seguimiento
  • Crea un controlador y vistas de Departments
  • Actualiza la vista de índice
  • Actualiza los métodos de edición
  • Actualiza la vista de edición
  • Prueba los conflictos de simultaneidad
  • Actualizar la página Delete
  • Actualizar las vistas Details y Create

Requisitos previos

Conflictos de simultaneidad

Los conflictos de simultaneidad ocurren cuando un usuario muestra los datos de una entidad para editarlos y, después, otro usuario actualiza los datos de la misma entidad antes de que el primer cambio del usuario se escriba en la base de datos. Si no habilita la detección de este tipo de conflictos, quien actualice la base de datos en último lugar sobrescribe los cambios del otro usuario. En muchas aplicaciones, el riesgo es aceptable: si hay pocos usuarios o pocas actualizaciones, o si no es realmente importante si se sobrescriben algunos cambios, el costo de programación para la simultaneidad puede superar el beneficio obtenido. En ese caso, no tendrá que configurar la aplicación para que controle los conflictos de simultaneidad.

Simultaneidad pesimista (bloqueo)

Si la aplicación necesita evitar la pérdida accidental de datos en casos de simultaneidad, una manera de hacerlo es usar los bloqueos de base de datos. Esto se denomina simultaneidad pesimista. Por ejemplo, antes de leer una fila de una base de datos, solicita un bloqueo de solo lectura o para acceso de actualización. Si bloquea una fila para acceso de actualización, no se permite que ningún otro usuario bloquee la fila como solo lectura o para acceso de actualización, porque recibirían una copia de los datos que se están modificando. Si bloquea una fila para acceso de solo lectura, otras personas también pueden bloquearla para acceso de solo lectura pero no para actualización.

Administrar los bloqueos tiene desventajas. Puede ser bastante complicado de programar. Se necesita un número significativo de recursos de administración de base de datos, y puede provocar problemas de rendimiento a medida que aumenta el número de usuarios de una aplicación. Por estos motivos, no todos los sistemas de administración de bases de datos admiten la simultaneidad pesimista. Entity Framework Core no proporciona ninguna compatibilidad integrada para ello y en este tutorial no se muestra cómo implementarla.

Simultaneidad optimista

La alternativa a la simultaneidad pesimista es la simultaneidad optimista. La simultaneidad optimista implica permitir que se produzcan conflictos de simultaneidad y reaccionar correctamente si ocurren. Por ejemplo, Jane visita la página Edit Department y cambia la cantidad de Budget para el departamento de inglés de 350.000,00 a 0,00 USD.

Changing budget to 0

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.

Changing start date to 2013

Jane hace clic en Save primero y ve su cambio cuando el explorador vuelve a la página de índice.

Budget changed to zero

Entonces, John hace clic en Save en una página Edit que sigue mostrando un presupuesto de 350.000,00 USD. Lo que sucede después viene determinado por cómo controla los conflictos de simultaneidad.

Algunas de las opciones se exponen a continuación:

  • Puede realizar un seguimiento de la propiedad que ha modificado un usuario y actualizar solo las columnas correspondientes de la base de datos.

    En el escenario de ejemplo, no se perdería ningún dato porque los dos usuarios actualizaron diferentes propiedades. La próxima vez que un usuario examine el departamento de inglés, verá los cambios tanto de Jane como de John: una fecha de inicio de 9/1/2013 y un presupuesto de cero dólares. Este método de actualización puede reducir el número de conflictos que pueden dar lugar a una pérdida de datos, pero no puede evitar la pérdida de datos si se realizan cambios paralelos a la misma propiedad de una entidad. Si Entity Framework funciona de esta manera o no, depende de cómo implemente el código de actualización. A menudo no resulta práctico en una aplicación web, porque puede requerir mantener grandes cantidades de estado con el fin de realizar un seguimiento de todos los valores de propiedad originales de una entidad, así como los valores nuevos. Mantener grandes cantidades de estado puede afectar al rendimiento de la aplicación porque requiere recursos del servidor o se deben incluir en la propia página web (por ejemplo, en campos ocultos) o en una cookie.

  • Puede permitir que los cambios de John sobrescriban los cambios de Jane.

    La próxima vez que un usuario examine el departamento de inglés, verá 9/1/2013 y el valor de 350.000,00 USD restaurado. Esto se denomina un escenario de Prevalece el cliente o Prevalece el último. (Todos los valores del cliente tienen prioridad sobre lo que aparece en el almacén de datos). Como se mencionó en la introducción de esta sección, si no hace ninguna codificación para el control de la simultaneidad, se realizará automáticamente.

  • Puede evitar que el cambio de John se actualice en la base de datos.

    Por lo general, debería mostrar un mensaje de error, mostrarle el estado actual de los datos y permitirle volver a aplicar los cambios si quiere realizarlos. Esto se denomina un escenario de Prevalece el almacén. (Los valores del almacén de datos tienen prioridad sobre los valores enviados por el cliente). En este tutorial implementará el escenario de Prevalece el almacén. Este método garantiza que ningún cambio se sobrescriba sin que se avise al usuario de lo que está sucediendo.

Detectar los conflictos de simultaneidad

Puede resolver los conflictos controlando las excepciones DbConcurrencyException que inicia Entity Framework. Para saber cuándo se producen dichas excepciones, Entity Framework debe ser capaz de detectar conflictos. Por lo tanto, debe configurar correctamente la base de datos y el modelo de datos. Algunas opciones para habilitar la detección de conflictos son las siguientes:

  • En la tabla de la base de datos, incluya una columna de seguimiento que pueda usarse para determinar si una fila ha cambiado. Después puede configurar Entity Framework para que incluya esa columna en la cláusula Where de los comandos Update o Delete de SQL.

    El tipo de datos de la columna de seguimiento suele ser rowversion. El valor rowversion es un número secuencial que se incrementa cada vez que se actualiza la fila. En un comando Update o Delete, la cláusula Where incluye el valor original de la columna de seguimiento (la versión de la fila original). Si otro usuario ha cambiado la fila que se está actualizando, el valor en la columna rowversion es diferente del valor original, por lo que la instrucción Update o Delete no puede encontrar la fila que se va a actualizar debido a la cláusula Where. Cuando Entity Framework encuentra que no se ha actualizado ninguna fila mediante el comando Update o Delete (es decir, cuando el número de filas afectadas es cero), lo interpreta como un conflicto de simultaneidad.

  • Configure Entity Framework para que incluya los valores originales de cada columna de la tabla en la cláusula Where de los comandos Update y Delete.

    Como se muestra en la primera opción, si algo en la fila ha cambiado desde que se leyó por primera, la cláusula Where no devolverá una fila para actualizar, lo cual Entity Framework interpreta como un conflicto de simultaneidad. Para las tablas de base de datos que tienen muchas columnas, este enfoque puede dar lugar a cláusulas Where muy grandes y puede requerir mantener grandes cantidades de estado. Tal y como se indicó anteriormente, el mantenimiento de grandes cantidades de estado puede afectar al rendimiento de la aplicación. Por tanto, generalmente este enfoque no se recomienda y no es el método usado en este tutorial.

    Si quiere implementar este enfoque para la simultaneidad, tendrá que marcar todas las propiedades de clave no principal de la entidad de la que quiere realizar un seguimiento de simultaneidad agregándoles el atributo ConcurrencyCheck. Este cambio permite que Entity Framework incluya todas las columnas en la cláusula Where de SQL de las instrucciones Update y Delete.

En el resto de este tutorial agregará una propiedad de seguimiento rowversion para la entidad Department, creará un controlador y vistas, y comprobará que todo funciona correctamente.

Agrega una propiedad de seguimiento

En Models/Department.cs, agregue una propiedad de seguimiento denominada RowVersion:

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

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

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

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

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

        public int? InstructorID { get; set; }

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

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

El atributo Timestamp especifica que esta columna se incluirá en la cláusula Where de los comandos Update y Delete enviados a la base de datos. El atributo se denomina Timestamp porque las versiones anteriores de SQL Server usaban un tipo de datos timestamp antes de que la rowversion de SQL lo sustituyera por otro. El tipo .NET de rowversion es una matriz de bytes.

Si prefiere usar la API fluida, puede usar el método IsConcurrencyToken (en Data/SchoolContext.cs) para especificar la propiedad de seguimiento, tal como se muestra en el ejemplo siguiente:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

Al agregar una propiedad cambió el modelo de base de datos, por lo que necesita realizar otra migración.

Guarde los cambios, compile el proyecto y, después, escriba los siguientes comandos en la ventana de comandos:

dotnet ef migrations add RowVersion
dotnet ef database update

Crea un controlador y vistas de Departments

Aplique la técnica scaffolding a un controlador y vistas de Departments como lo hizo anteriormente para Students, Courses e Instructors.

Scaffold Department

En el archivo DepartmentsController.cs, cambie las cuatro repeticiones de "FirstMidName" a "FullName" para que las listas desplegables del administrador del departamento contengan el nombre completo del instructor en lugar de simplemente el apellido.

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);

Actualiza la vista de índice

El motor de scaffolding ha creado una columna RowVersion en la vista Index, pero ese campo no debería mostrarse.

Reemplace el código de Views/Departments/Index.cshtml por el código siguiente.

@model IEnumerable<ContosoUniversity.Models.Department>

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

<h2>Departments</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Administrator)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Esto cambia el encabezado por "Departments", elimina la columna RowVersion y muestra el nombre completo en lugar del nombre del administrador.

Actualiza los métodos de edición

En el método Edit de HttpGet y el método Details, agregue AsNoTracking. En el método Edit de HttpGet, agregue carga diligente para el administrador.

var department = await _context.Departments
    .Include(i => i.Administrator)
    .AsNoTracking()
    .FirstOrDefaultAsync(m => m.DepartmentID == id);

Sustituya el código existente para el método Edit de HttpPost por el siguiente código:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

    var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate,
        "",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var exceptionEntry = ex.Entries.Single();
            var clientValues = (Department)exceptionEntry.Entity;
            var databaseEntry = exceptionEntry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

El código comienza por intentar leer el departamento que se va a actualizar. Si el método FirstOrDefaultAsync devuelve NULL, otro usuario eliminó el departamento. En ese caso, el código usa los valores de formulario publicados para crear una entidad Department, por lo que puede volver a mostrarse la página Edit con un mensaje de error. Como alternativa, no tendrá que volver a crear la entidad Department si solo muestra un mensaje de error sin volver a mostrar los campos del departamento.

La vista almacena el valor RowVersion original en un campo oculto, y este método recibe ese valor en el parámetro rowVersion. Antes de llamar a SaveChanges, tendrá que colocar dicho valor de propiedad RowVersion original en la colección OriginalValues para la entidad.

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

Cuando Entity Framework crea un comando UPDATE de SQL, ese comando incluirá una cláusula WHERE que comprueba si hay una fila que tenga el valor RowVersion original. Si no hay ninguna fila afectada por el comando UPDATE (ninguna fila tiene el valor RowVersion original), Entity Framework inicia una excepción DbUpdateConcurrencyException.

El código del bloque catch de esa excepción obtiene la entidad Department afectada que tiene los valores actualizados de la propiedad Entries del objeto de excepción.

var exceptionEntry = ex.Entries.Single();

La colección Entries contará con un solo objeto EntityEntry. Puede usar dicho objeto para obtener los nuevos valores especificados por el usuario y los valores actuales de la base de datos.

var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();

El código agrega un mensaje de error personalizado para cada columna que tenga valores de base de datos diferentes de lo que el usuario especificó en la página Edit (aquí solo se muestra un campo por razones de brevedad).

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

Por último, el código establece el valor RowVersion de departmentToUpdate para el nuevo valor recuperado de la base de datos. Este nuevo valor RowVersion se almacenará en el campo oculto cuando se vuelva a mostrar la página Edit y, la próxima vez que el usuario haga clic en Save, solo se detectarán los errores de simultaneidad que se produzcan desde que se vuelva a mostrar la página Edit.

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

La instrucción ModelState.Remove es necesaria porque ModelState tiene el valor RowVersion antiguo. En la vista, el valor ModelState de un campo tiene prioridad sobre los valores de propiedad de modelo cuando ambos están presentes.

Actualiza la vista de edición

En Views/Departments/Edit.cshtml, realice los cambios siguientes:

  • Agregue un campo oculto para guardar el valor de propiedad RowVersion, inmediatamente después de un campo oculto para la propiedad DepartmentID.

  • Agregue una opción "Select Administrator" a la lista desplegable.

@model ContosoUniversity.Models.Department

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

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="InstructorID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

Prueba los conflictos de simultaneidad

Ejecute la aplicación y vaya a la página de índice de Departments. Haga clic con el botón derecho en el hipervínculo Edit del departamento de inglés, seleccione Abrir en nueva pestaña y, después, haga clic en el hipervínculo Edit del departamento de inglés. Las dos pestañas del explorador ahora muestran la misma información.

Cambie un campo en la primera pestaña del explorador y haga clic en Save.

Department Edit page 1 after change

El explorador muestra la página de índice con el valor modificado.

Cambie un campo en la segunda pestaña del explorador.

Department Edit page 2 after change

Haga clic en Save(Guardar). Verá un mensaje de error:

Department Edit page error message

Vuelva a hacer clic en Save. Se guarda el valor especificado en la segunda pestaña del explorador. Verá los valores guardados cuando aparezca la página de índice.

Actualizar la página Delete

Para la página Delete, Entity Framework detecta los conflictos de simultaneidad causados por una persona que edita el departamento de forma similar. Cuando el método Delete de HttpGet muestra la vista de confirmación, la vista incluye el valor RowVersion original en un campo oculto. Dicho valor está entonces disponible para el método Delete de HttpPost al que se llama cuando el usuario confirma la eliminación. Cuando Entity Framework crea el comando DELETE de SQL, incluye una cláusula WHERE con el valor RowVersion original. Si el comando tiene como resultado cero filas afectadas (es decir, la fila se cambió después de que se muestre la página de confirmación de eliminación), se produce una excepción de simultaneidad y el método Delete de HttpGet se llama con una marca de error establecida en true para volver a mostrar la página de confirmación con un mensaje de error. También es posible que se vieran afectadas cero filas porque otro usuario eliminó la fila, por lo que en ese caso no se muestra ningún mensaje de error.

Actualizar los métodos Delete en el controlador de Departments

En DepartmentsController.cs, reemplace el método Delete de HttpGet por el código siguiente:

public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return NotFound();
    }

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

El método acepta un parámetro opcional que indica si la página volverá a aparecer después de un error de simultaneidad. Si esta marca es true y el departamento especificado ya no existe, significa que otro usuario lo eliminó. En ese caso, el código redirige a la página de índice. Si esta marca es true y el departamento existe, significa que otro usuario lo ha cambiado. En ese caso, el código envía un mensaje de error a la vista mediante ViewData.

Reemplace el código en el método Delete de HttpPost (denominado DeleteConfirmed) con el código siguiente:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}

En el código al que se aplicó la técnica scaffolding que acaba de reemplazar, este método solo acepta un identificador de registro:

public async Task<IActionResult> DeleteConfirmed(int id)

Ha cambiado este parámetro por una instancia de la entidad Department creada por el enlazador de modelos. Esto proporciona a EF acceso al valor de la propiedad RowVersion, además de la clave de registro.

public async Task<IActionResult> Delete(Department department)

También ha cambiado el nombre del método de acción de DeleteConfirmed a Delete. El código al que se aplicó la técnica scaffolding usa el nombre DeleteConfirmed para proporcionar al método HttpPost una firma única. (El CLR requiere métodos sobrecargados para tener parámetros de método diferentes). Ahora que las firmas son únicas, puede ceñirse a la convención MVC y usar el mismo nombre para los métodos de eliminación de HttpPost y HttpGet.

Si ya se ha eliminado el departamento, el método AnyAsync devuelve false y la aplicación simplemente vuelve al método de índice.

Si se detecta un error de simultaneidad, el código vuelve a mostrar la página de confirmación de Delete y proporciona una marca que indica que se debería mostrar un mensaje de error de simultaneidad.

Actualizar la vista Delete

En Views/Departments/Delete.cshtml, reemplace el código al que se aplicó la técnica scaffolding con el siguiente código, que agrega un campo de mensaje de error y campos ocultos para las propiedades DepartmentID y RowVersion. Los cambios aparecen resaltados.

@model ContosoUniversity.Models.Department

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

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="DepartmentID" />
        <input type="hidden" asp-for="RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

Esto realiza los cambios siguientes:

  • Agrega un mensaje de error entre los encabezados h2 y h3.

  • Reemplaza FirstMidName por FullName en el campo Administrator.

  • Quita el campo RowVersion.

  • Agrega un campo oculto para la propiedad RowVersion.

Ejecute la aplicación y vaya a la página de índice de Departments. Haga clic con el botón derecho en el hipervínculo Delete del departamento de inglés, seleccione Abrir en nueva pestaña y, después, en la primera pestaña, haga clic en el hipervínculo Edit del departamento de inglés.

En la primera ventana, cambie uno de los valores y haga clic en Save:

Department Edit page after change before delete

En la segunda pestaña, haga clic en Delete. Verá el mensaje de error de simultaneidad y se actualizarán los valores de Department con lo que está actualmente en la base de datos.

Department Delete confirmation page with concurrency error

Si vuelve a hacer clic en Delete, se le redirigirá a la página de índice, que muestra que se ha eliminado el departamento.

Actualizar las vistas Details y Create

Si quiere, puede limpiar el código al que se ha aplicado la técnica scaffolding en las vistas Details y Create.

Reemplace el código de Views/Departments/Details.cshtml para eliminar la columna RowVersion y mostrar el nombre completo del administrador.

@model ContosoUniversity.Models.Department

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

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Reemplace el código de Views/Departments/Create.cshtml para agregar una opción Select en la lista desplegable.

@model ContosoUniversity.Models.Department

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

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

Obtención del código

Descargue o vea la aplicación completa.

Recursos adicionales

Para obtener más información sobre cómo administrar la simultaneidad en EF Core, vea Conflictos de simultaneidad.

Pasos siguientes

En este tutorial ha:

  • Obtenido información sobre los conflictos de simultaneidad
  • Agregado una propiedad de seguimiento
  • Creado un controlador y vistas de Departments
  • Actualizado la vista de índice
  • Actualizado los métodos de edición
  • Actualizado la vista de edición
  • Probado los conflictos de simultaneidad
  • Actualizado la página Delete
  • Actualizado las vistas Details y Create

Pase al tutorial siguiente para obtener información sobre cómo implementar la herencia de tabla por jerarquía para las entidades Instructor y Student.