Руководство. Обработка параллелизма — ASP.NET MVC с помощью EF Core

В предыдущих учебниках вы узнали, как обновлять данные. Это руководство описывает, как обрабатывать конфликты, когда несколько пользователей одновременно изменяют одну сущность.

Вы создадите веб-страницы, которые работают с сущностью Department и обрабатывают ошибки параллелизма. На следующих рисунках показаны страницы "Edit" (Редактирование) и "Delete" (Удаление), включая некоторые сообщения, которые отображаются при возникновении конфликта параллелизма.

Department Edit page

Department Delete page

Изучив это руководство, вы:

  • Дополнительные сведения о конфликтах параллелизма
  • Добавление свойства отслеживания
  • Создание представлений и контроллера кафедр
  • Обновление представления указателя
  • Обновление методов редактирования
  • Обновление представления редактирования
  • Тестирование конфликтов параллелизма
  • Обновление страницы "Delete" (Удаление)
  • Обновление представлений Details и Create

Необходимые компоненты

Конфликты параллелизма

Конфликт параллелизма возникает, когда один пользователь отображает данные сущности, чтобы изменить их, а другой пользователь обновляет данные той же сущности до того, как изменение первого пользователя будет записано в базу данных. Если не включить обнаружение таких конфликтов, то пользователь, обновляющий базу данных последним, перезаписывает изменения другого пользователя. Во многих приложениях такой риск допустим: при небольшом числе пользователей или обновлений, а также в случае, если перезапись некоторых изменений не является критической, стоимость реализации параллелизма может перевесить его преимущества. В этом случае вам не нужно настраивать приложение для обработки конфликтов параллелизма.

Пессимистичный параллелизм (блокировка)

Если приложению нужно предотвратить случайную потерю данных в сценариях параллелизма, одним из способов сделать это являются блокировки базы данных. Это называется пессимистичным параллелизмом. Например, перед чтением строки из базы данных вы запрашиваете блокировку для доступа для обновления или только для чтения. Если заблокировать строку для обновления, другие пользователи не могут заблокировать ее для обновления или только для чтения, так как получат копию данных, которые находятся в процессе изменения. Если заблокировать строку только для чтения, другие пользователи также могут заблокировать ее только для чтения, но не для обновления.

Управление блокировками имеет недостатки. Оно может оказаться сложным с точки зрения программирования. Оно требует значительных ресурсов управления базами данных, а также может вызвать проблемы с производительностью по мере увеличения числа пользователей приложения. Поэтому не все системы управления базами данных поддерживают пессимистичный параллелизм. В Entity Framework Core нет встроенной поддержки этой функции, и данное руководство не рассказывает, как ее реализовать.

Оптимистическая блокировка

Альтернативой пессимистичному параллелизму является оптимистичный параллелизм (оптимистическая блокировка). Оптимистическая блокировка допускает появление конфликтов параллелизма, а затем обрабатывает их соответствующим образом. Например, Мария посещает страницу изменения кафедры и изменяет бюджет кафедры английской языка с 350 000,00 USD на 0,00 USD.

Changing budget to 0

Прежде чем Мария нажимает кнопку Save (Сохранить), Дмитрий заходит на ту же страницу и изменяет значение в поле "Start Date" (Дата начала) с 9/1/2007 на 9/1/2013.

Changing start date to 2013

Сначала Мария нажимает кнопку Save (Сохранить) и видит свое изменение, когда браузер возвращается на страницу индекса.

Budget changed to zero

Затем Дмитрий нажимает кнопку Save (Сохранить) на странице редактирования, где все еще отображается бюджет 350 000,00 USD. Дальнейший ход событий определяется порядком обработки конфликтов параллелизма.

Некоторые параметры перечислены ниже:

  • Вы можете отслеживать, для какого свойства пользователь изменил и обновил только соответствующие столбцы в базе данных.

    В этом примере сценария данные не будут потеряны, так как эти два пользователя обновляли разные свойства. Когда какой-либо пользователь просмотрит кафедру английского языка в следующий раз, он увидит изменения, внесенные как Марией, так и Дмитрием — дату начала 9/1/2013 и бюджет в нуль долларов США. Этот метод обновления помогает снизить число конфликтов, которые могут привести к потере данных, но не позволяет избежать такой потери, когда конкурирующие изменения вносятся в одно свойство сущности. То, работает ли Entity Framework в таком режиме, зависит от того, как вы реализуете код обновления. В веб-приложении это часто нецелесообразно, так как может потребоваться обрабатывать большой объем состояний, чтобы отслеживать все исходные значения свойств для сущности, а также новые значения. Обработка большого объема состояний может повлиять на производительность приложения, так как требует ресурсов сервера или должна быть включена непосредственно в веб-страницу (например, в скрытые поля) или файл cookie.

  • Вы можете позволить изменению Дмитрия перезаписать изменение Марии.

    Когда какой-либо пользователь просмотрит кафедру английского языка в следующий раз, он увидит дату 9/1/2013 и восстановленное значение 350 000,00 USD. Такой подход называется победой клиента или сохранением последнего внесенного изменения. (Все значения от клиента имеют приоритет над тем, что находится в хранилище данных.) Как отмечалось в этом разделе, если вы не выполняете код для обработки параллелизма, это произойдет автоматически.

  • Вы можете запретить обновление изменения Дмитрия в базе данных.

    Как правило, следует отобразить сообщение об ошибке, показать текущее состояние данных и позволить ему повторно применить свои изменения, если они ему нужны. Это называется победой хранилища. (Значения хранилища данных имеют приоритет над значениями, отправленными клиентом.) Вы реализуете сценарий Магазина Wins в этом руководстве. Данный метод гарантирует, что никакие изменения не перезаписываются без оповещения пользователя о случившемся.

Обнаружение конфликтов параллелизма

Конфликты можно разрешать путем обработки исключений DbConcurrencyException, выдаваемых Entity Framework. Чтобы определить, когда именно нужно выдавать исключения, платформа Entity Framework должна быть в состоянии обнаруживать конфликты. Поэтому нужно соответствующим образом настроить базу данных и модель данных. Ниже приведены некоторые варианты для реализации обнаружения конфликтов:

  • Включите в таблицу базы данных столбец отслеживания, который позволяет определять, когда была изменена строка. Затем можно настроить Entity Framework для включения этого столбца в предложение Where команд SQL Update или Delete.

    Типом данных для столбца отслеживания обычно является rowversion. Значение rowversion является последовательным номером, увеличивающимся при каждом обновлении строки. В команде Update или Delete предложение Where содержит исходное значение столбца отслеживания (исходную версию строки). Если обновляемая строка была изменена другим пользователем, значение в столбце rowversion отличается от исходного значения, поэтому оператору Update или Delete не удается найти строку для обновления из-за предложения Where. Когда платформа Entity Framework обнаруживает, что ни одна из строк не была обновлена с помощью команды Update или Delete (то есть число затронутых строк равно нулю), она интерпретирует это как конфликт параллелизма.

  • Настройте Entity Framework для включения исходного значения каждого столбца таблицы в предложение Where команд Update и Delete.

    Как и в случае с первым вариантом, если с момента первого чтения в строке что-либо изменилось, предложение Where не возвращает строку для обновления, что платформа Entity Framework интерпретирует как конфликт параллелизма. Для таблиц базы данных со множеством столбцов этот подход может привести к очень большим предложениям Where и потребовать обрабатывать большой объем состояний. Как было указано ранее, обслуживание большого объема состояний может негативно повлиять на производительность приложения. Поэтому в общем случае данный подход не рекомендуется, кроме того, он не применяется и в этом руководстве.

    Если вы хотите реализовать этот подход к параллелизму, нужно пометить все свойства, не относящиеся к первичному ключу, в сущности, где требуется отслеживать параллелизм, добавив к ним атрибут ConcurrencyCheck. Это изменение позволяет Entity Framework включить все столбцы в предложение Where SQL операторов Update и Delete.

В оставшейся части этого руководства вам предстоит добавить свойство отслеживания rowversion в сущность Department, создать контроллер и представления, а также проверить правильность работы решения.

Добавление свойства отслеживания

Добавьте Models/Department.csсвойство отслеживания с именем 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; }
    }
}

Атрибут Timestamp указывает, что этот столбец будет включен в предложение Where команд Update и Delete, отправляемых в базу данных. Этот атрибут называется Timestamp, так как предыдущие версии SQL Server использовали тип данных timestamp SQL, пока его не сменил rowversion. Тип .NET для rowversion — это массив байтов.

Если вы предпочитаете использовать текучий API, можно воспользоваться методом IsConcurrencyTokenData/SchoolContext.cs), чтобы указать свойство отслеживания, как показано в следующем примере:

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

Добавив свойство, вы изменили модель базы данных, поэтому нужно выполнить еще одну миграцию.

Сохраните изменения, выполните сборку проекта и введите в командном окне следующие команды:

dotnet ef migrations add RowVersion
dotnet ef database update

Создание представлений и контроллера кафедр

Сформируйте шаблоны для контроллера кафедр и представлений, как делали это раньше для учащихся, курсов и преподавателей.

Scaffold Department

DepartmentsController.cs В файле измените все четыре вхождения FirstMidName на FullName, чтобы раскрывающиеся списки администратора отдела содержали полное имя инструктора, а не только фамилию.

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

Обновление представления указателя

Подсистема формирования шаблонов создала столбец RowVersion в представлении индекса, однако это поле не должно отображаться.

Замените код в Views/Departments/Index.cshtml следующим:

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

Он изменяет заголовок на "Departments" (Кафедры), удаляет столбец RowVersion и отображает администратору имя и фамилию, а не только имя.

Обновление методов редактирования

Добавьте AsNoTracking в оба метода — HttpGet Edit и Details. В методе HttpGet Edit добавьте безотложную загрузку для администратора.

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

Замените существующий код в методе HttpPostEdit следующим кодом:

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

Код начинает пытаться считать кафедру для обновления. Если метод FirstOrDefaultAsync возвращает значение null, кафедра была удалена другим пользователем. В этом случае код использует переданные значения из формы для создания сущности Department, чтобы страницу редактирования можно было отобразить повторно с сообщением об ошибке. Как вариант, можно повторно не создавать сущность Department, если вы выводите только сообщение об ошибке без повторного отображения полей кафедры.

Представление сохраняет исходное значение RowVersion в скрытом поле, а данный метод получает это значение в параметре rowVersion. Перед вызовом SaveChanges нужно поместить это исходное значение свойства RowVersion в коллекцию OriginalValues для сущности.

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

Когда позднее Entity Framework создает команду SQL UPDATE, она будет содержать предложение WHERE, которое ищет строку с исходным значением RowVersion. Если команда UPDATE не затрагивает никакие строки (нет строк, имеющих исходное значение RowVersion), Entity Framework выдает исключение DbUpdateConcurrencyException.

Код в блоке catch для этого исключения возвращает затронутую сущность Department, имеющую обновленные значения из свойства Entries в объекте исключения.

var exceptionEntry = ex.Entries.Single();

Коллекция Entries будет иметь всего один объект EntityEntry. Вы можете использовать его для получения новых значений, введенных пользователем, и текущих значений в базе данных.

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

Код добавляет настраиваемое сообщение об ошибке для каждого столбца, для которого значения базы данных отличаются от введенных пользователем на странице "Edit" (Редактирование) (здесь для краткости показано всего одно поле).

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

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

Наконец, код задает для RowVersion объекта departmentToUpdate новое значение, полученное из базы данных. Это новое значение RowVersion будет сохранено в скрытом поле при повторном отображении страницы "Edit" (Редактирование). Когда пользователь в следующий раз нажимает кнопку Save (Сохранить), перехватываются только те ошибки параллелизма, которые возникли с момента повторного отображения страницы "Edit" (Редактирование).

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

Оператор ModelState.Remove является обязательным, так как ModelState имеет старое значение RowVersion. На представлении значение ModelState для поля имеет приоритет над значениями свойств модели, если они присутствуют вместе.

Обновление представления редактирования

В Views/Departments/Edit.cshtml внесите следующие изменения:

  • Добавьте скрытое поле для сохранения значения свойства RowVersion сразу после скрытого поля для свойства DepartmentID.

  • Добавьте пункт "Select Administrator" (Выбрать администратора) в раскрывающийся список.

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

Тестирование конфликтов параллелизма

Запустите приложение и перейдите на страницу индекса кафедр. Щелкните правой кнопкой мыши гиперссылку Edit (Изменить) для кафедры английского языка и выберите пункт Открыть на новой вкладке, а затем щелкните гиперссылку Edit (Изменить) для этой кафедры. Теперь на обеих вкладках браузера отображаются одинаковые сведения.

Измените поле на первой вкладке браузера и нажмите кнопку Save (Сохранить).

Department Edit page 1 after change

В браузере отображается страница индекса с измененным значением.

Измените поле на второй вкладке браузера.

Department Edit page 2 after change

Щелкните Сохранить. Отображается сообщение об ошибке:

Department Edit page error message

Щелкните Сохранить еще раз. Сохраняется значение, введенное на второй вкладке браузера. Сохраненные значения отображаются при открытии страницы индекса.

Обновление страницы "Delete" (Удаление)

Для страницы "Delete" (Удаление) платформа Entity Framework обнаруживает конфликты параллелизма, вызванные схожим изменением кафедры. Когда метод HttpGet Delete отображает представление подтверждения, оно содержит исходное значение RowVersion в скрытом поле. Затем это значение становится доступным для метода HttpPost Delete, вызываемого при подтверждении удаления пользователем. Когда Entity Framework создает команду SQL DELETE, она включает предложение WHERE с исходным значением RowVersion. Если команда не затрагивает ни одной строки (подразумевается изменение строки после отображения страницы подтверждения удаления), возникает исключение параллелизма и вызывается метод HttpGet Delete, у которого для флага ошибки установлено значение true, чтобы повторно отобразить страницу подтверждения с сообщением об ошибке. Отсутствие затронутых строк также может быть вызвано тем, что строка была удалена другим пользователем, и в этом случае сообщение об ошибке не отображается.

Обновление методов Delete в контроллере кафедр

Замените DepartmentsController.csметод HttpGet Delete следующим кодом:

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

Этот метод принимает необязательный параметр, который указывает, отображается ли страница повторно после ошибки параллелизма. Если этот флаг имеет значение true, а указанная кафедра больше не существует, она была удалена другим пользователем. В этом случае код выполняет перенаправление на страницу индекса. Если этот флаг имеет значение true, а кафедра существует, она была изменена другим пользователем. В этом случае код отправляет сообщение об ошибке в представление, используя ViewData.

Замените код в методе HttpPostDelete (с именем DeleteConfirmed) следующим кодом:

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

В шаблонном коде, который вы только что заменили, этот метод принимал только идентификатор записи:

public async Task<IActionResult> DeleteConfirmed(int id)

Вы изменили этот параметр на экземпляр сущности Department, созданный связывателем модели. Это предоставляет EF доступ к значению свойства RowVersion в дополнение к ключу записи.

public async Task<IActionResult> Delete(Department department)

Вы также изменили имя метода действия с DeleteConfirmed на Delete. Шаблонный код использовал имя DeleteConfirmed, чтобы предоставить методу HttpPost уникальную сигнатуру. (Среда CLR требует, чтобы перегруженные методы имели разные параметры метода.) Теперь, когда подписи уникальны, вы можете придерживаться соглашения MVC и использовать то же имя для методов удаления HttpPost и HttpGet.

Если кафедра уже удалена, метод AnyAsync возвращает значение false, а приложение просто возвращается к методу Index.

При перехвате ошибки параллелизма код повторно отображает страницу подтверждения удаления и предоставляет флаг, указывающий, что нужно отобразить сообщение об ошибке параллелизма.

Обновление представления удаления

Замените Views/Departments/Delete.cshtmlшаблонный код следующим кодом, который добавляет поле сообщения об ошибке и скрытые поля для свойств DepartmentID и RowVersion. Изменения выделены.

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

Этот код вносит следующие изменения:

  • Добавляет сообщение об ошибке между заголовками h2 и h3.

  • Заменяет FirstMidName на FullName в поле Administrator (Администратор).

  • Удаляет поле RowVersion.

  • Добавляет скрытое поле для свойства RowVersion.

Запустите приложение и перейдите на страницу индекса кафедр. Щелкните правой кнопкой мыши гиперссылку Delete (Удалить) для кафедры английского языка и выберите пункт Открыть на новой вкладке, а затем на первой вкладке щелкните гиперссылку Edit (Изменить) для этой кафедры.

В первом окне измените одно из значений и нажмите кнопку Save (Сохранить):

Department Edit page after change before delete

На второй вкладке нажмите кнопку Delete (Удалить). Вы видите сообщение об ошибке параллелизма, а значения кафедры обновляются с использованием актуальных сведений из базы данных.

Department Delete confirmation page with concurrency error

Если нажать кнопку Delete (Удалить) еще раз, вы будете перенаправлены на страницу индекса, которая показывает, что кафедра была удалена.

Обновление представлений Details и Create

При необходимости вы можете очистить шаблонный код в представлениях Details и Create.

Замените кодViews/Departments/Details.cshtml, чтобы удалить столбец RowVersion и показать полное имя Администратор istrator.

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

Замените код, Views/Departments/Create.cshtml чтобы добавить параметр Select в раскрывающийся список.

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

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

Дополнительные ресурсы

Дополнительные сведения об обработке параллелизма см. в EF Coreразделе "Конфликты параллелизма".

Следующие шаги

Изучив это руководство, вы:

  • Дополнительные сведения о конфликтах параллелизма
  • Добавление свойства отслеживания
  • Создание представлений и контроллера кафедр
  • Обновление представления указателя
  • Обновление методов редактирования
  • Обновление представления редактирования
  • Тестирование конфликтов параллелизма
  • Обновление страницы удаления
  • Обновление представлений сведений и создания

В следующем учебнике описано, как реализовать наследование "одна таблица на иерархию" для сущностей Instructor и Student.