Tutorial: lidar com a simultaneidade - ASP.NET MVC com EF Core

Nos tutoriais anteriores, você aprendeu a atualizar dados. Este tutorial mostra como lidar com conflitos quando os mesmos usuários atualizam a mesma entidade simultaneamente.

Você criará páginas da Web que funcionam com a entidade Department e tratará erros de simultaneidade. As ilustrações a seguir mostram as páginas Editar e Excluir, incluindo algumas mensagens exibidas se ocorre um conflito de simultaneidade.

Department Edit page

Department Delete page

Neste tutorial, você:

  • Aprender sobre conflitos de simultaneidade
  • Adicionar uma propriedade de acompanhamento
  • Criar um controlador e exibições de Departamentos
  • Atualizar a exibição do Índice
  • Atualizar métodos de Edição
  • Atualizar a exibição de Edição
  • Testar os conflitos de simultaneidade
  • Atualizar a página Excluir
  • Exibições Atualizar Detalhes e Criar

Pré-requisitos

Conflitos de simultaneidade

Um conflito de simultaneidade ocorre quando um usuário exibe dados de uma entidade para editá-los e, em seguida, outro usuário atualiza os mesmos dados da entidade antes que a primeira alteração do usuário seja gravada no banco de dados. Se você não habilitar a detecção desses conflitos, a última pessoa que atualizar o banco de dados substituirá as outras alterações do usuário. Em muitos aplicativos, esse risco é aceitável: se houver poucos usuários ou poucas atualizações ou se não for realmente crítico se algumas alterações forem substituídas, o custo de programação para simultaneidade poderá superar o benefício. Nesse caso, você não precisa configurar o aplicativo para lidar com conflitos de simultaneidade.

Simultaneidade pessimista (bloqueio)

Se o aplicativo precisar evitar a perda acidental de dados em cenários de simultaneidade, uma maneira de fazer isso será usar bloqueios de banco de dados. Isso é chamado de simultaneidade pessimista. Por exemplo, antes de ler uma linha de um banco de dados, você solicita um bloqueio para o acesso somente leitura ou de atualização. Se você bloquear uma linha para o acesso de atualização, nenhum outro usuário terá permissão para bloquear a linha para o acesso somente leitura ou de atualização, porque ele obterá uma cópia dos dados que estão sendo alterados. Se você bloquear uma linha para o acesso somente leitura, outros também poderão bloqueá-la para o acesso somente leitura, mas não para atualização.

O gerenciamento de bloqueios traz desvantagens. Ele pode ser complexo de ser programado. Exige recursos de gerenciamento de banco de dados significativos e pode causar problemas de desempenho, conforme o número de usuários de um aplicativo aumenta. Por esses motivos, nem todos os sistemas de gerenciamento de banco de dados dão suporte à simultaneidade pessimista. O Entity Framework Core não fornece nenhum suporte interno para ele, e este tutorial não mostra como implementá-lo.

Simultaneidade otimista

A alternativa à simultaneidade pessimista é a simultaneidade otimista. Simultaneidade otimista significa permitir que conflitos de simultaneidade ocorram e responder adequadamente se eles ocorrerem. Por exemplo, Alice visita a página Editar Departamento e altera o valor do Orçamento para o departamento de inglês de US$ 350.000,00 para US$ 0,00.

Changing budget to 0

Antes que Alice clique em Salvar, Julio visita a mesma página e altera o campo Data de Início de 1/9/2007 para 1/9/2013.

Changing start date to 2013

Alice clica em Salvar primeiro e vê a alteração quando o navegador retorna à página Índice.

Budget changed to zero

Em seguida, Julio clica em Salvar em uma página Editar que ainda mostra um orçamento de US$ 350.000,00. O que acontece em seguida é determinado pela forma como você lida com conflitos de simultaneidade.

Algumas das opções incluem o seguinte:

  • Controle qual propriedade um usuário modificou e atualize apenas as colunas correspondentes no banco de dados.

    No cenário de exemplo, nenhum dado é perdido, porque propriedades diferentes foram atualizadas pelos dois usuários. Na próxima vez que alguém navegar pelo departamento de inglês, verá as alterações de Alice e de Julio – a data de início de 1/9/2013 e um orçamento de zero dólar. Esse método de atualização pode reduzir a quantidade de conflitos que podem resultar em perda de dados, mas ele não poderá evitar a perda de dados se forem feitas alterações concorrentes à mesma propriedade de uma entidade. Se o Entity Framework funciona dessa maneira depende de como o código de atualização é implementado. Geralmente, isso não é prático em um aplicativo Web, porque pode exigir que você mantenha grandes quantidades de estado para manter o controle de todos os valores de propriedade originais de uma entidade, bem como novos valores. A manutenção de grandes quantidades de estado pode afetar o desempenho do aplicativo, pois exige recursos do servidor ou deve ser incluída na própria página da Web (por exemplo, em campos ocultos) ou em um cookie.

  • Você não pode deixar a alteração de Julio substituir a alteração de Alice.

    Na próxima vez que alguém navegar pelo departamento de inglês, verá 1/9/2013 e o valor de US$ 350.000,00 restaurado. Isso é chamado de um cenário O cliente vence ou O último vence. (Todos os valores do cliente têm precedência sobre o conteúdo do armazenamento de dados.) Conforme observado na introdução a esta seção, se você não fizer nenhuma codificação para a manipulação de simultaneidade, isso ocorrerá automaticamente.

  • Você pode impedir que as alterações de Julio sejam atualizadas no banco de dados.

    Normalmente, você exibirá uma mensagem de erro, mostrará a ele o estado atual dos dados e permitirá a ele aplicar as alterações novamente se ele ainda desejar fazê-las. Isso é chamado de um cenário O armazenamento vence. (Os valores do armazenamento de dados têm precedência sobre os valores enviados pelo cliente.) Você implementará o cenário O armazenamento vence neste tutorial. Esse método garante que nenhuma alteração é substituída sem que um usuário seja alertado sobre o que está acontecendo.

Detectando conflitos de simultaneidade

Resolva conflitos manipulando exceções DbConcurrencyException geradas pelo Entity Framework. Para saber quando gerar essas exceções, o Entity Framework precisa poder detectar conflitos. Portanto, é necessário configurar o banco de dados e o modelo de dados de forma adequada. Algumas opções para habilitar a detecção de conflitos incluem as seguintes:

  • Na tabela de banco de dados, inclua uma coluna de acompanhamento que pode ser usada para determinar quando uma linha é alterada. Em seguida, configure o Entity Framework para incluir essa coluna na cláusula Where de comandos SQL Update ou Delete.

    O tipo de dados da coluna de acompanhamento é normalmente rowversion. O valor rowversion é um número sequencial que é incrementado sempre que a linha é atualizada. Em um comando Update ou Delete, a cláusula Where inclui o valor original da coluna de acompanhamento (a versão de linha original). Se a linha que está sendo atualizada tiver sido alterada por outro usuário, o valor da coluna rowversion será diferente do valor original; portanto, a instrução Update ou Delete não poderá encontrar a linha a ser atualizada devido à cláusula Where. Quando o Entity Framework descobre que nenhuma linha foi atualizada pelo comando Update ou Delete (ou seja, quando o número de linhas afetadas é zero), ele interpreta isso como um conflito de simultaneidade.

  • Configure o Entity Framework para incluir os valores originais de cada coluna na tabela na cláusula Where de comandos Update e Delete.

    Como a primeira opção, se nada na linha for alterado desde que a linha tiver sido lida pela primeira vez, a cláusula Where não retornará uma linha a ser atualizada, o que o Entity Framework interpretará como um conflito de simultaneidade. Para tabelas de banco de dados que têm muitas colunas, essa abordagem pode resultar em cláusulas Where muito grandes e pode exigir que você mantenha grandes quantidades de estado. Conforme observado anteriormente, manter grandes quantidades de estado pode afetar o desempenho do aplicativo. Portanto, essa abordagem geralmente não é recomendada e não é o método usado neste tutorial.

    Caso deseje implementar essa abordagem, precisará marcar todas as propriedades de chave não primária na entidade para as quais você deseja controlar a simultaneidade adicionando o atributo ConcurrencyCheck a elas. Essa alteração permite que o Entity Framework inclua todas as colunas na cláusula Where do SQL de instruções Update e Delete.

No restante deste tutorial, você adicionará uma propriedade de acompanhamento rowversion à entidade Department, criará um controlador e exibições e testará para verificar se tudo está funcionando corretamente.

Adicionar uma propriedade de acompanhamento

Em Models/Department.cs, adicione uma propriedade de controle chamada 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; }
    }
}

O atributo Timestamp especifica que essa coluna será incluída na cláusula Where de comandos Update e Delete enviados ao banco de dados. O atributo é chamado Timestamp porque as versões anteriores do SQL Server usavam um tipo de dados timestamp do SQL antes de o rowversion do SQL substituí-lo. O tipo do .NET para rowversion é uma matriz de bytes.

Se preferir usar a API fluente, use o método IsConcurrencyToken (em Data/SchoolContext.cs) para especificar a propriedade de acompanhamento, conforme mostrado no seguinte exemplo:

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

Adicionando uma propriedade, você alterou o modelo de banco de dados e, portanto, precisa fazer outra migração.

Salve as alterações e compile o projeto e, em seguida, insira os seguintes comandos na janela Comando:

dotnet ef migrations add RowVersion
dotnet ef database update

Criar um controlador e exibições de Departamentos

Gere por scaffolding um controlador e exibições Departamentos, como você fez anteriormente para Alunos, Cursos e Instrutores.

Scaffold Department

No arquivo DepartmentsController.cs, altere todas as quatro ocorrências de "FirstMidName" para "FullName", de modo que as listas suspensas do administrador do departamento contenham o nome completo do instrutor em vez de apenas o sobrenome.

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

Atualizar a exibição do Índice

O mecanismo de scaffolding criou uma coluna RowVersion na exibição Índice, mas esse campo não deve ser exibido.

Substitua o código em Views/Departments/Index.cshtml pelo seguinte código.

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

Isso altera o título "Departamentos", exclui a coluna RowVersion e mostra o nome completo em vez de o nome do administrador.

Atualizar métodos de Edição

Nos métodos HttpGet Edit e Details, adicione AsNoTracking. No método HttpGet Edit, adicione o carregamento adiantado ao Administrador.

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

Substitua o código existente do método HttpPost Edit pelo seguinte 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);
}

O código começa com a tentativa de ler o departamento a ser atualizado. Se o método FirstOrDefaultAsync retornar nulo, isso indicará que o departamento foi excluído por outro usuário. Nesse caso, o código usa os valores de formulário postados para criar uma entidade Department, de modo que a página Editar possa ser exibida novamente com uma mensagem de erro. Como alternativa, você não precisará recriar a entidade Department se exibir apenas uma mensagem de erro sem exibir novamente os campos de departamento.

A exibição armazena o valor RowVersion original em um campo oculto e esse método recebe esse valor no parâmetro rowVersion. Antes de chamar SaveChanges, você precisa colocar isso no valor da propriedade RowVersion original na coleção OriginalValues da entidade.

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

Em seguida, quando o Entity Framework criar um comando SQL UPDATE, esse comando incluirá uma cláusula WHERE que procura uma linha que tem o valor RowVersion original. Se nenhuma linha for afetada pelo comando UPDATE (nenhuma linha tem o valor RowVersion original), o Entity Framework gerará uma exceção DbUpdateConcurrencyException.

O código no bloco catch dessa exceção obtém a entidade Department afetada que tem os valores atualizados da propriedade Entries no objeto de exceção.

var exceptionEntry = ex.Entries.Single();

A coleção Entries terá apenas um objeto EntityEntry. Use esse objeto para obter os novos valores inseridos pelo usuário e os valores de banco de dados atuais.

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

O código adiciona uma mensagem de erro personalizada a cada coluna que tem valores de banco de dados diferentes do que o usuário inseriu na página Editar (apenas um campo é mostrado aqui para fins de brevidade).

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

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

Por fim, o código define o valor RowVersion do departmentToUpdate com o novo valor recuperado do banco de dados. Esse novo valor RowVersion será armazenado no campo oculto quando a página Editar for exibida novamente, e na próxima vez que o usuário clicar em Salvar, somente os erros de simultaneidade que ocorrem desde a nova exibição da página Editar serão capturados.

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

A instrução ModelState.Remove é obrigatória porque ModelState tem o valor RowVersion antigo. Na exibição, o valor ModelState de um campo tem precedência sobre os valores de propriedade do modelo, quando ambos estão presentes.

Atualizar a exibição de Edição

Em Views/Departments/Edit.cshtml, faça as seguintes alterações:

  • Adicione um campo oculto para salvar o valor da propriedade RowVersion, imediatamente após o campo oculto da propriedade DepartmentID.

  • Adicione uma opção "Selecionar Administrador" à lista suspensa.

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

Testar os conflitos de simultaneidade

Execute o aplicativo e acesse a página Índice de Departamentos. Clique com o botão direito do mouse na hiperlink Editar do departamento de inglês e selecione Abrir em uma nova guia e, em seguida, clique no hiperlink Editar no departamento de inglês. As duas guias do navegador agora exibem as mesmas informações.

Altere um campo na primeira guia do navegador e clique em Salvar.

Department Edit page 1 after change

O navegador mostra a página Índice com o valor alterado.

Altere um campo na segunda guia do navegador.

Department Edit page 2 after change

Clique em Save (Salvar). Você verá uma mensagem de erro:

Department Edit page error message

Clique em Salvar novamente. O valor inserido na segunda guia do navegador foi salvo. Você verá os valores salvos quando a página Índice for exibida.

Atualizar a página Excluir

Para a página Excluir, o Entity Framework detecta conflitos de simultaneidade causados pela edição por outra pessoa do departamento de maneira semelhante. Quando o método HttpGet Delete exibe a exibição de confirmação, a exibição inclui o valor RowVersion original em um campo oculto. Em seguida, esse valor estará disponível para o método HttpPost Delete chamado quando o usuário confirmar a exclusão. Quando o Entity Framework cria o comando SQL DELETE, ele inclui uma cláusula WHERE com o valor RowVersion original. Se o comando não resultar em nenhuma linha afetada (o que significa que a linha foi alterada após a exibição da página Confirmação de exclusão), uma exceção de simultaneidade será gerada e o método HttpGet Delete será chamado com um sinalizador de erro definido como verdadeiro para exibir novamente a página de confirmação com uma mensagem de erro. Também é possível que nenhuma linha tenha sido afetada porque a linha foi excluída por outro usuário; portanto, nesse caso, nenhuma mensagem de erro é exibida.

Atualizar os métodos Excluir no controlador Departamentos

Em DepartmentsController.cs, substitua o método HttpGet Delete pelo código a seguir:

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

O método aceita um parâmetro opcional que indica se a página está sendo exibida novamente após um erro de simultaneidade. Se esse sinalizador é verdadeiro e o departamento especificado não existe mais, isso indica que ele foi excluído por outro usuário. Nesse caso, o código redireciona para a página Índice. Se esse sinalizador é verdadeiro e o departamento existe, isso indica que ele foi alterado por outro usuário. Nesse caso, o código envia uma mensagem de erro para a exibição usando ViewData.

Substitua o código no método HttpPost Delete (chamado DeleteConfirmed) pelo seguinte código:

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

No código gerado por scaffolding que acabou de ser substituído, esse método aceitou apenas uma ID de registro:

public async Task<IActionResult> DeleteConfirmed(int id)

Você alterou esse parâmetro para uma instância da entidade Department criada pelo associador de modelos. Isso concede o acesso ao EF ao valor da propriedade RowVersion, além da chave de registro.

public async Task<IActionResult> Delete(Department department)

Você também alterou o nome do método de ação de DeleteConfirmed para Delete. O código gerado por scaffolding usou o nome DeleteConfirmed para fornecer ao método HttpPost uma assinatura exclusiva. (O CLR exige que métodos sobrecarregados tenham diferentes parâmetros de método.) Agora que as assinaturas são exclusivas, você pode continuar com a convenção MVC e usar o mesmo nome para os métodos de exclusão HttpPost e HttpGet.

Se o departamento já foi excluído, o método AnyAsync retorna falso e o aplicativo apenas volta para o método de Índice.

Se um erro de simultaneidade é capturado, o código exibe novamente a página Confirmação de exclusão e fornece um sinalizador que indica que ela deve exibir uma mensagem de erro de simultaneidade.

Atualizar a exibição Excluir

Em Views/Departments/Delete.cshtml, substitua o código gerado por scaffolding pelo seguinte código que adiciona um campo de mensagem de erro e campos ocultos às propriedades DepartmentID e RowVersion. As alterações são realçadas.

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

Isso faz as seguintes alterações:

  • Adiciona uma mensagem de erro entre os cabeçalhos h2 e h3.

  • Substitua FirstMidName por FullName no campo Administrador.

  • Remove o campo RowVersion.

  • Adiciona um campo oculto à propriedade RowVersion.

Execute o aplicativo e acesse a página Índice de Departamentos. Clique com o botão direito do mouse na hiperlink Excluir do departamento de inglês e selecione Abrir em uma nova guia e, em seguida, na primeira guia, clique no hiperlink Editar no departamento de inglês.

Na primeira janela, altere um dos valores e clique em Salvar:

Department Edit page after change before delete

Na segunda guia, clique em Excluir. Você verá a mensagem de erro de simultaneidade e os valores de Departamento serão atualizados com o que está atualmente no banco de dados.

Department Delete confirmation page with concurrency error

Se você clicar em Excluir novamente, será redirecionado para a página Índice, que mostra que o departamento foi excluído.

Exibições Atualizar Detalhes e Criar

Opcionalmente, você pode limpar o código gerado por scaffolding nas exibições Detalhes e Criar.

Substitua o código em Views/Departments/Details.cshtml para excluir a coluna RowVersion e mostrar o nome completo do 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>

Substitua o código em Views/Departments/Create.cshtml para adicionar uma opção Selecionar à lista suspensa.

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

Obter o código

Baixe ou exiba o aplicativo concluído.

Recursos adicionais

Para obter mais informações sobre como lidar com a simultaneidade no EF Core, consulte Conflitos de simultaneidade.

Próximas etapas

Neste tutorial, você:

  • Aprendeu sobre conflitos de simultaneidade
  • Adicionou uma propriedade de acompanhamento
  • Criou um controlador e exibições de Departamentos
  • Atualizou a exibição do Índice
  • Atualizou métodos de Edição
  • Atualizou a exibição de Edição
  • Testou conflitos de simultaneidade
  • Atualizou a página Excluir
  • Atualizou as exibições de Detalhes e Criar

Vá para o próximo tutorial para aprender a implementar a herança de tabela por hierarquia para as entidades Instructor e Student.