Parte 2, Razor Pages com EF Core no ASP.NET Core – CRUD

Por Tom Dykstra, Jeremy Likness e Jon P Smith

O aplicativo Web Contoso University demonstra como criar aplicativos Web das Razor Pages usando o EF Core e o Visual Studio. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial.

Se você encontrar problemas que não possa resolver, baixe o aplicativo concluído e compare esse código com o que você criou seguindo o tutorial.

Neste tutorial, o código CRUD (criar, ler, atualizar e excluir) gerado por scaffolding é examinado e personalizado.

Nenhum repositório

Alguns desenvolvedores usam um padrão de repositório ou camada de serviço para criar uma camada de abstração entre a interface do usuário (Razor Pages) e a camada de acesso a dados. Este tutorial não faz isso. Para minimizar a complexidade e manter o tutorial focado em EF Core, o código EF Core é adicionado diretamente às classes de modelo de página.

Atualizar a página Detalhes

O código com scaffold das páginas Alunos não inclui dados de registro. Nesta seção, os registros são adicionados à página Details.

Ler inscrições

Para exibir os dados de registro de um aluno na página, os dados de registro devem ser lidos. O código scaffolded no Pages/Students/Details.cshtml.cs lê apenas os dados Student, sem os dados Enrollment:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

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

Substitua o método OnGetAsync pelo código a seguir para ler os dados de registro para o aluno selecionado. As alterações são realçadas.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

Os métodos Include e ThenInclude fazem com que o contexto carregue a propriedade de navegação Student.Enrollments e, dentro de cada registro, a propriedade de navegação Enrollment.Course. Esses métodos são examinados em detalhes no tutorial Ler dados relacionados.

O método AsNoTracking melhora o desempenho em cenários em que as entidades retornadas não são atualizadas no contexto atual. AsNoTracking é abordado mais adiante neste tutorial.

Exibir inscrições

Substitua o código em Pages/Students/Details.cshtml pelo código a seguir para exibir uma lista de inscrições. As alterações são realçadas.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

O código anterior percorre as entidades na propriedade de navegação Enrollments. Para cada registro, ele exibe o nome do curso e a nota. O título do curso é recuperado da entidade Course, que é armazenada na propriedade de navegação Course da entidade Enrollments.

Execute o aplicativo, selecione a guia Alunos e clique no link Detalhes de um aluno. A lista de cursos e notas do aluno selecionado é exibida.

Maneiras de ler uma entidade

O código gerado usa FirstOrDefaultAsync para ler uma entidade. Esse método retornará null se nada for encontrado; caso contrário, retornará a primeira linha encontrada que atenda aos critérios de filtro de consulta. FirstOrDefaultAsync geralmente é uma opção melhor do que as seguintes alternativas:

  • SingleOrDefaultAsync – gera uma exceção se houver mais de uma entidade que atenda ao filtro de consulta. Para determinar se mais de uma linha poderia ser retornada pela consulta, o SingleOrDefaultAsync tenta buscar várias linhas. Esse trabalho extra será desnecessário se a consulta só puder retornar uma entidade, como quando ela pesquisa em uma chave exclusiva.
  • FindAsync – localiza uma entidade com a PK (chave primária). Se uma entidade com o PK estiver sendo controlada pelo contexto, ela será retornada sem uma solicitação para o banco de dados. Esse método é otimizado para pesquisar uma única entidade, mas você não pode chamar Include com FindAsync. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync será a melhor opção.

Rotear dados versus cadeia de consulta

A URL para a página Detalhes é https://localhost:<port>/Students/Details?id=1. O valor da chave primária da entidade está na cadeia de consulta. Alguns desenvolvedores preferem passar o valor da chave nos dados da rota: https://localhost:<port>/Students/Details/1. Para obter mais informações, confira Atualizar o código gerado.

Atualizar a página Criar

O código OnPostAsync com scaffold para a página Criar é vulnerável à sobreposição. Substitua o método OnPostAsync em Pages/Students/Create.cshtml.cs pelo código a seguir.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

O código anterior cria um objeto Student e, em seguida, usa campos de formulário postados para atualizar as propriedades do objeto Student. O método TryUpdateModelAsync:

  • Usa os valores de formulário postados da propriedade PageContext no PageModel.
  • Atualiza apenas as propriedades listadas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Procura campos de formulário com um prefixo "Student". Por exemplo, Student.FirstMidName. Não diferencia maiúsculas de minúsculas.
  • Usa o sistema de model binding para converter valores de formulário de cadeias de caracteres para os tipos no modelo Student. Por exemplo, EnrollmentDate é convertido para DateTime.

Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.

Excesso de postagem

O uso de TryUpdateModel para atualizar campos com valores postados é uma melhor prática de segurança porque ele impede o excesso de postagem. Por exemplo, suponha que a entidade Student inclua uma propriedade Secret que esta página da Web não deve atualizar nem adicionar:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Mesmo que o aplicativo não tenha um campo Secret em criar ou atualizar Razor Page, um invasor pode definir o valor Secret por excesso de postagem. Um invasor pode usar uma ferramenta como o Fiddler ou escrever um JavaScript para postar um valor de formulário Secret. O código original não limita os campos que o associador de modelos usa quando ele cria uma instância Student.

Seja qual for o valor que o invasor especificou para o campo de formulário Secret, ele será atualizado no banco de dados. A imagem a seguir mostra a ferramenta Fiddler adicionando o campo Secret com o valor "OverPost" aos valores de formulário postados.

Fiddler adding Secret field

O valor "OverPost" foi adicionado com êxito à propriedade Secret da linha inserida. Isso acontece embora o designer de aplicativo nunca tenha pretendido que a propriedade Secret fosse definida com a página Criar.

Exibir modelo

Os modelos de exibição fornecem uma maneira alternativa para impedir o excesso de postagem.

O modelo de aplicativo costuma ser chamado de modelo de domínio. O modelo de domínio normalmente contém todas as propriedades necessárias para a entidade correspondente no banco de dados. O modelo de exibição contém apenas as propriedades necessárias para a página de interface do usuário, por exemplo, a página Criar.

Além do modelo de exibição, alguns aplicativos usam um modelo de associação ou modelo de entrada para passar dados entre a classe de modelo de página do Razor Pages e o navegador.

Considere o seguinte modelo de exibição StudentVM:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

O seguinte código usa o modelo de exibição StudentVM para criar um novo aluno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

O método SetValues define os valores desse objeto lendo os valores de outro objeto PropertyValues. SetValues usa a correspondência de nomes de propriedade. O tipo de modelo de exibição:

  • Não precisa estar relacionado ao tipo de modelo.
  • Precisa ter propriedades que correspondam.

O uso de StudentVM requer o uso da página Criar StudentVM em vez de Student:

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

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

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

Atualizar a página Editar

No Pages/Students/Edit.cshtml.cs, substitua os métodos OnGetAsync e OnPostAsync pelo código a seguir.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

As alterações de código são semelhantes à página Criar, com algumas exceções:

  • FirstOrDefaultAsync foi substituído por FindAsync. Quando você não precisa incluir dados relacionados, FindAsync é mais eficiente.
  • OnPostAsync tem um parâmetro id.
  • O aluno atual é buscado do banco de dados, em vez de criar um aluno vazio.

Execute o aplicativo e teste-o criando e editando um aluno.

Estados da entidade

O contexto de banco de dados controla se as entidades em memória estão em sincronia com suas linhas correspondentes no banco de dados. As informações de acompanhamento determinam o que acontece quando SaveChangesAsync é chamado. Por exemplo, quando uma nova entidade é passada para o método AddAsync, o estado da entidade é definido como Added. Quando SaveChangesAsync é chamado, o contexto de banco de dados emite um comando SQL INSERT.

Uma entidade pode estar em um dos seguintes estados:

  • Added: a entidade ainda não existe no banco de dados. O método SaveChanges emite uma instrução INSERT.

  • Unchanged: nenhuma alteração precisa ser salva com essa entidade. Uma entidade tem esse status quando é lida do banco de dados.

  • Modified: alguns ou todos os valores de propriedade da entidade foram modificados. O método SaveChanges emite uma instrução UPDATE.

  • Deleted: a entidade foi marcada para exclusão. O método SaveChanges emite uma instrução DELETE.

  • Detached: a entidade não está sendo controlada pelo contexto de banco de dados.

Em um aplicativo da área de trabalho, em geral, as alterações de estado são definidas automaticamente. Uma entidade é lida, as alterações são feitas e o estado da entidade é alterado automaticamente para Modified. A chamada a SaveChanges gera uma instrução SQL UPDATE que atualiza apenas as propriedades alteradas.

Em um aplicativo Web, o DbContext que lê uma entidade e exibe os dados é descartado depois que uma página é renderizada. Quando o método OnPostAsync de uma página é chamado, é feita uma nova solicitação da Web e com uma nova instância do DbContext. A nova leitura da entidade nesse novo contexto simula o processamento da área de trabalho.

Atualizar a página Excluir

Nesta seção, uma mensagem de erro personalizada é implementada quando há falha na chamada a SaveChanges.

Substitua o código em Pages/Students/Delete.cshtml.cs pelo seguinte código:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

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

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

O código anterior:

  • Adiciona registro em log.
  • Adiciona o parâmetro saveChangesError opcional à assinatura do método OnGetAsync. saveChangesError indica se o método foi chamado após uma falha ao excluir o objeto de aluno.

A operação de exclusão pode falhar devido a problemas de rede temporários. Erros de rede transitórios são mais prováveis quando o banco de dados está na nuvem. O parâmetro saveChangesError é false quando a página Excluir OnGetAsync é chamada na interface do usuário. Quando OnGetAsync é chamado por OnPostAsync devido à falha da operação de exclusão, o parâmetro saveChangesError é true.

O método OnPostAsync recupera a entidade selecionada e, em seguida, chama o método Remove para definir o status da entidade como Deleted. Quando SaveChanges é chamado, um comando SQL DELETE é gerado. Se Remove falhar:

  • A exceção de banco de dados é capturada.
  • O método OnGetAsync das páginas Excluir é chamado com saveChangesError=true.

Adicione uma mensagem de erro a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Execute o aplicativo e exclua um aluno para testar a página Excluir.

Próximas etapas

Neste tutorial, o código CRUD (criar, ler, atualizar e excluir) gerado por scaffolding é examinado e personalizado.

Nenhum repositório

Alguns desenvolvedores usam um padrão de repositório ou camada de serviço para criar uma camada de abstração entre a interface do usuário (Razor Pages) e a camada de acesso a dados. Este tutorial não faz isso. Para minimizar a complexidade e manter o tutorial focado em EF Core, o código EF Core é adicionado diretamente às classes de modelo de página.

Atualizar a página Detalhes

O código com scaffold das páginas Alunos não inclui dados de registro. Nesta seção, os registros são adicionados à página Details.

Ler inscrições

Para exibir os dados de registro de um aluno na página, os dados de registro devem ser lidos. O código scaffolded no Pages/Students/Details.cshtml.cs lê apenas os dados Student, sem os dados Enrollment:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

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

Substitua o método OnGetAsync pelo código a seguir para ler os dados de registro para o aluno selecionado. As alterações são realçadas.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

Os métodos Include e ThenInclude fazem com que o contexto carregue a propriedade de navegação Student.Enrollments e, dentro de cada registro, a propriedade de navegação Enrollment.Course. Esses métodos são examinados em detalhes no tutorial Ler dados relacionados.

O método AsNoTracking melhora o desempenho em cenários em que as entidades retornadas não são atualizadas no contexto atual. AsNoTracking é abordado mais adiante neste tutorial.

Exibir inscrições

Substitua o código em Pages/Students/Details.cshtml pelo código a seguir para exibir uma lista de inscrições. As alterações são realçadas.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

O código anterior percorre as entidades na propriedade de navegação Enrollments. Para cada registro, ele exibe o nome do curso e a nota. O título do curso é recuperado da entidade Course, que é armazenada na propriedade de navegação Course da entidade Enrollments.

Execute o aplicativo, selecione a guia Alunos e clique no link Detalhes de um aluno. A lista de cursos e notas do aluno selecionado é exibida.

Maneiras de ler uma entidade

O código gerado usa FirstOrDefaultAsync para ler uma entidade. Esse método retornará null se nada for encontrado; caso contrário, retornará a primeira linha encontrada que atenda aos critérios de filtro de consulta. FirstOrDefaultAsync geralmente é uma opção melhor do que as seguintes alternativas:

  • SingleOrDefaultAsync – gera uma exceção se houver mais de uma entidade que atenda ao filtro de consulta. Para determinar se mais de uma linha poderia ser retornada pela consulta, o SingleOrDefaultAsync tenta buscar várias linhas. Esse trabalho extra será desnecessário se a consulta só puder retornar uma entidade, como quando ela pesquisa em uma chave exclusiva.
  • FindAsync – localiza uma entidade com a PK (chave primária). Se uma entidade com o PK estiver sendo controlada pelo contexto, ela será retornada sem uma solicitação para o banco de dados. Esse método é otimizado para pesquisar uma única entidade, mas você não pode chamar Include com FindAsync. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync será a melhor opção.

Rotear dados versus cadeia de consulta

A URL para a página Detalhes é https://localhost:<port>/Students/Details?id=1. O valor da chave primária da entidade está na cadeia de consulta. Alguns desenvolvedores preferem passar o valor da chave nos dados da rota: https://localhost:<port>/Students/Details/1. Para obter mais informações, confira Atualizar o código gerado.

Atualizar a página Criar

O código OnPostAsync com scaffold para a página Criar é vulnerável à sobreposição. Substitua o método OnPostAsync em Pages/Students/Create.cshtml.cs pelo código a seguir.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

O código anterior cria um objeto Student e, em seguida, usa campos de formulário postados para atualizar as propriedades do objeto Student. O método TryUpdateModelAsync:

  • Usa os valores de formulário postados da propriedade PageContext no PageModel.
  • Atualiza apenas as propriedades listadas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Procura campos de formulário com um prefixo "Student". Por exemplo, Student.FirstMidName. Não diferencia maiúsculas de minúsculas.
  • Usa o sistema de model binding para converter valores de formulário de cadeias de caracteres para os tipos no modelo Student. Por exemplo, EnrollmentDate é convertido para DateTime.

Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.

Excesso de postagem

O uso de TryUpdateModel para atualizar campos com valores postados é uma melhor prática de segurança porque ele impede o excesso de postagem. Por exemplo, suponha que a entidade Student inclua uma propriedade Secret que esta página da Web não deve atualizar nem adicionar:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Mesmo que o aplicativo não tenha um campo Secret em criar ou atualizar Razor Page, um invasor pode definir o valor Secret por excesso de postagem. Um invasor pode usar uma ferramenta como o Fiddler ou escrever um JavaScript para postar um valor de formulário Secret. O código original não limita os campos que o associador de modelos usa quando ele cria uma instância Student.

Seja qual for o valor que o invasor especificou para o campo de formulário Secret, ele será atualizado no banco de dados. A imagem a seguir mostra a ferramenta Fiddler adicionando o campo Secret com o valor "OverPost" aos valores de formulário postados.

Fiddler adding Secret field

O valor "OverPost" foi adicionado com êxito à propriedade Secret da linha inserida. Isso acontece embora o designer de aplicativo nunca tenha pretendido que a propriedade Secret fosse definida com a página Criar.

Exibir modelo

Os modelos de exibição fornecem uma maneira alternativa para impedir o excesso de postagem.

O modelo de aplicativo costuma ser chamado de modelo de domínio. O modelo de domínio normalmente contém todas as propriedades necessárias para a entidade correspondente no banco de dados. O modelo de exibição contém apenas as propriedades necessárias para a página de interface do usuário, por exemplo, a página Criar.

Além do modelo de exibição, alguns aplicativos usam um modelo de associação ou modelo de entrada para passar dados entre a classe de modelo de página do Razor Pages e o navegador.

Considere o seguinte modelo de exibição StudentVM:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

O seguinte código usa o modelo de exibição StudentVM para criar um novo aluno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

O método SetValues define os valores desse objeto lendo os valores de outro objeto PropertyValues. SetValues usa a correspondência de nomes de propriedade. O tipo de modelo de exibição:

  • Não precisa estar relacionado ao tipo de modelo.
  • Precisa ter propriedades que correspondam.

O uso de StudentVM requer o uso da página Criar StudentVM em vez de Student:

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

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

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

Atualizar a página Editar

No Pages/Students/Edit.cshtml.cs, substitua os métodos OnGetAsync e OnPostAsync pelo código a seguir.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

As alterações de código são semelhantes à página Criar, com algumas exceções:

  • FirstOrDefaultAsync foi substituído por FindAsync. Quando você não precisa incluir dados relacionados, FindAsync é mais eficiente.
  • OnPostAsync tem um parâmetro id.
  • O aluno atual é buscado do banco de dados, em vez de criar um aluno vazio.

Execute o aplicativo e teste-o criando e editando um aluno.

Estados da entidade

O contexto de banco de dados controla se as entidades em memória estão em sincronia com suas linhas correspondentes no banco de dados. As informações de acompanhamento determinam o que acontece quando SaveChangesAsync é chamado. Por exemplo, quando uma nova entidade é passada para o método AddAsync, o estado da entidade é definido como Added. Quando SaveChangesAsync é chamado, o contexto de banco de dados emite um comando SQL INSERT.

Uma entidade pode estar em um dos seguintes estados:

  • Added: a entidade ainda não existe no banco de dados. O método SaveChanges emite uma instrução INSERT.

  • Unchanged: nenhuma alteração precisa ser salva com essa entidade. Uma entidade tem esse status quando é lida do banco de dados.

  • Modified: alguns ou todos os valores de propriedade da entidade foram modificados. O método SaveChanges emite uma instrução UPDATE.

  • Deleted: a entidade foi marcada para exclusão. O método SaveChanges emite uma instrução DELETE.

  • Detached: a entidade não está sendo controlada pelo contexto de banco de dados.

Em um aplicativo da área de trabalho, em geral, as alterações de estado são definidas automaticamente. Uma entidade é lida, as alterações são feitas e o estado da entidade é alterado automaticamente para Modified. A chamada a SaveChanges gera uma instrução SQL UPDATE que atualiza apenas as propriedades alteradas.

Em um aplicativo Web, o DbContext que lê uma entidade e exibe os dados é descartado depois que uma página é renderizada. Quando o método OnPostAsync de uma página é chamado, é feita uma nova solicitação da Web e com uma nova instância do DbContext. A nova leitura da entidade nesse novo contexto simula o processamento da área de trabalho.

Atualizar a página Excluir

Nesta seção, uma mensagem de erro personalizada é implementada quando há falha na chamada a SaveChanges.

Substitua o código em Pages/Students/Delete.cshtml.cs pelo seguinte código:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

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

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

O código anterior:

  • Adiciona registro em log.
  • Adiciona o parâmetro saveChangesError opcional à assinatura do método OnGetAsync. saveChangesError indica se o método foi chamado após uma falha ao excluir o objeto de aluno.

A operação de exclusão pode falhar devido a problemas de rede temporários. Erros de rede transitórios são mais prováveis quando o banco de dados está na nuvem. O parâmetro saveChangesError é false quando a página Excluir OnGetAsync é chamada na interface do usuário. Quando OnGetAsync é chamado por OnPostAsync devido à falha da operação de exclusão, o parâmetro saveChangesError é true.

O método OnPostAsync recupera a entidade selecionada e, em seguida, chama o método Remove para definir o status da entidade como Deleted. Quando SaveChanges é chamado, um comando SQL DELETE é gerado. Se Remove falhar:

  • A exceção de banco de dados é capturada.
  • O método OnGetAsync das páginas Excluir é chamado com saveChangesError=true.

Adicione uma mensagem de erro a Pages/Students/Delete.cshtml:

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Execute o aplicativo e exclua um aluno para testar a página Excluir.

Próximas etapas

Neste tutorial, o código CRUD (criar, ler, atualizar e excluir) gerado por scaffolding é examinado e personalizado.

Nenhum repositório

Alguns desenvolvedores usam um padrão de repositório ou camada de serviço para criar uma camada de abstração entre a interface do usuário (Razor Pages) e a camada de acesso a dados. Este tutorial não faz isso. Para minimizar a complexidade e manter o tutorial focado em EF Core, o código EF Core é adicionado diretamente às classes de modelo de página.

Atualizar a página Detalhes

O código com scaffold das páginas Alunos não inclui dados de registro. Nesta seção, os registros são adicionados à página Detalhes.

Ler inscrições

Para exibir os dados de registro de um aluno na página, os dados de registro devem ser lidos. O código com scaffold em Pages/Students/Details.cshtml.cs lê somente os dados do Aluno, sem os dados de Registro:

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

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

Substitua o método OnGetAsync pelo código a seguir para ler os dados de registro para o aluno selecionado. As alterações são realçadas.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

Os métodos Include e ThenInclude fazem com que o contexto carregue a propriedade de navegação Student.Enrollments e, dentro de cada registro, a propriedade de navegação Enrollment.Course. Esses métodos são examinados em detalhes no tutorial Como ler dados relacionado.

O método AsNoTracking melhora o desempenho em cenários em que as entidades retornadas não são atualizadas no contexto atual. AsNoTracking é abordado mais adiante neste tutorial.

Exibir inscrições

Substitua o código em Pages/Students/Details.cshtml pelo código a seguir para exibir uma lista de inscrições. As alterações são realçadas.

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

O código anterior percorre as entidades na propriedade de navegação Enrollments. Para cada registro, ele exibe o nome do curso e a nota. O título do curso é recuperado da entidade Course, que é armazenada na propriedade de navegação Course da entidade Enrollments.

Execute o aplicativo, selecione a guia Alunos e clique no link Detalhes de um aluno. A lista de cursos e notas do aluno selecionado é exibida.

Maneiras de ler uma entidade

O código gerado usa FirstOrDefaultAsync para ler uma entidade. Esse método retornará null se nada for encontrado; caso contrário, retornará a primeira linha encontrada que atenda aos critérios de filtro de consulta. FirstOrDefaultAsync geralmente é uma opção melhor do que as seguintes alternativas:

  • SingleOrDefaultAsync – gera uma exceção se houver mais de uma entidade que atenda ao filtro de consulta. Para determinar se mais de uma linha poderia ser retornada pela consulta, o SingleOrDefaultAsync tenta buscar várias linhas. Esse trabalho extra será desnecessário se a consulta só puder retornar uma entidade, como quando ela pesquisa em uma chave exclusiva.
  • FindAsync – localiza uma entidade com a PK (chave primária). Se uma entidade com o PK estiver sendo controlada pelo contexto, ela será retornada sem uma solicitação para o banco de dados. Esse método é otimizado para pesquisar uma única entidade, mas você não pode chamar Include com FindAsync. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync será a melhor opção.

Rotear dados versus cadeia de consulta

A URL para a página Detalhes é https://localhost:<port>/Students/Details?id=1. O valor da chave primária da entidade está na cadeia de consulta. Alguns desenvolvedores preferem passar o valor da chave nos dados da rota: https://localhost:<port>/Students/Details/1. Para obter mais informações, confira Atualizar o código gerado.

Atualizar a página Criar

O código OnPostAsync com scaffold para a página Criar é vulnerável à sobreposição. Substitua o método OnPostAsync em Pages/Students/Create.cshtml.cs pelo código a seguir.

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

O código anterior cria um objeto Student e, em seguida, usa campos de formulário postados para atualizar as propriedades do objeto Student. O método TryUpdateModelAsync:

  • Usa os valores de formulário postados da propriedade PageContext no PageModel.
  • Atualiza apenas as propriedades listadas (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate).
  • Procura campos de formulário com um prefixo "Student". Por exemplo, Student.FirstMidName. Não diferencia maiúsculas de minúsculas.
  • Usa o sistema de model binding para converter valores de formulário de cadeias de caracteres para os tipos no modelo Student. Por exemplo, EnrollmentDate deve ser convertido em DateTime.

Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.

Excesso de postagem

O uso de TryUpdateModel para atualizar campos com valores postados é uma melhor prática de segurança porque ele impede o excesso de postagem. Por exemplo, suponha que a entidade Student inclua uma propriedade Secret que esta página da Web não deve atualizar nem adicionar:

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

Mesmo que o aplicativo não tenha um campo Secret em criar ou atualizar Razor Page, um invasor pode definir o valor Secret por excesso de postagem. Um invasor pode usar uma ferramenta como o Fiddler ou escrever um JavaScript para postar um valor de formulário Secret. O código original não limita os campos que o associador de modelos usa quando ele cria uma instância Student.

Seja qual for o valor que o invasor especificou para o campo de formulário Secret, ele será atualizado no banco de dados. A imagem a seguir mostra a ferramenta Fiddler adicionando o campo Secret (com o valor "OverPost") aos valores de formulário postados.

Fiddler adding Secret field

O valor "OverPost" foi adicionado com êxito à propriedade Secret da linha inserida. Isso acontece embora o designer de aplicativo nunca tenha pretendido que a propriedade Secret fosse definida com a página Criar.

Exibir modelo

Os modelos de exibição fornecem uma maneira alternativa para impedir o excesso de postagem.

O modelo de aplicativo costuma ser chamado de modelo de domínio. O modelo de domínio normalmente contém todas as propriedades necessárias para a entidade correspondente no banco de dados. O modelo de exibição contém apenas as propriedades necessárias para a interface do usuário que é usada (por exemplo, a página Criar).

Além do modelo de exibição, alguns aplicativos usam um modelo de associação ou modelo de entrada para passar dados entre a classe de modelo de página do Razor Pages e o navegador.

Considere o seguinte modelo de exibição Student:

using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

O seguinte código usa o modelo de exibição StudentVM para criar um novo aluno:

[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

O método SetValues define os valores desse objeto lendo os valores de outro objeto PropertyValues. SetValues usa a correspondência de nomes de propriedade. O tipo de modelo de exibição não precisa estar relacionado ao tipo de modelo, apenas precisa ter as propriedades correspondentes.

Usar StudentVM requer atualizar Create.cshtml para usar StudentVM em vez de Student.

Atualizar a página Editar

No Pages/Students/Edit.cshtml.cs, substitua os métodos OnGetAsync e OnPostAsync pelo código a seguir.

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

As alterações de código são semelhantes à página Criar, com algumas exceções:

  • FirstOrDefaultAsync foi substituído por FindAsync. Quando os dados relacionados incluídos não são necessários, FindAsync é mais eficiente.
  • OnPostAsync tem um parâmetro id.
  • O aluno atual é buscado do banco de dados, em vez de criar um aluno vazio.

Execute o aplicativo e teste-o criando e editando um aluno.

Estados da entidade

O contexto de banco de dados controla se as entidades em memória estão em sincronia com suas linhas correspondentes no banco de dados. As informações de acompanhamento determinam o que acontece quando SaveChangesAsync é chamado. Por exemplo, quando uma nova entidade é passada para o método AddAsync, o estado da entidade é definido como Added. Quando SaveChangesAsync é chamado, o contexto de banco de dados emite um comando SQL INSERT.

Uma entidade pode estar em um dos seguintes estados:

  • Added: a entidade ainda não existe no banco de dados. O método SaveChanges emite uma instrução INSERT.

  • Unchanged: nenhuma alteração precisa ser salva com essa entidade. Uma entidade tem esse status quando é lida do banco de dados.

  • Modified: alguns ou todos os valores de propriedade da entidade foram modificados. O método SaveChanges emite uma instrução UPDATE.

  • Deleted: a entidade foi marcada para exclusão. O método SaveChanges emite uma instrução DELETE.

  • Detached: a entidade não está sendo controlada pelo contexto de banco de dados.

Em um aplicativo da área de trabalho, em geral, as alterações de estado são definidas automaticamente. Uma entidade é lida, as alterações são feitas e o estado da entidade é alterado automaticamente para Modified. A chamada a SaveChanges gera uma instrução SQL UPDATE que atualiza apenas as propriedades alteradas.

Em um aplicativo Web, o DbContext que lê uma entidade e exibe os dados é descartado depois que uma página é renderizada. Quando o método OnPostAsync de uma página é chamado, é feita uma nova solicitação da Web e com uma nova instância do DbContext. A nova leitura da entidade nesse novo contexto simula o processamento da área de trabalho.

Atualizar a página Excluir

Nesta seção, você implementa uma mensagem de erro personalizada quando há falha na chamada a SaveChanges.

Substitua o código em Pages/Students/Delete.cshtml.cs pelo seguinte código. As alterações são realçadas (além da limpeza de instruções using).

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

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

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = "Delete failed. Try again";
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

O código anterior adiciona o parâmetro saveChangesError opcional à assinatura do método OnGetAsync. saveChangesError indica se o método foi chamado após uma falha ao excluir o objeto de aluno. A operação de exclusão pode falhar devido a problemas de rede temporários. Erros de rede transitórios são mais prováveis quando o banco de dados está na nuvem. O parâmetro saveChangesError é falso quando a página Excluir OnGetAsync é chamada na interface do usuário. Quando OnGetAsync é chamado por OnPostAsync (devido à falha da operação de exclusão), o parâmetro saveChangesError é verdadeiro.

O método OnPostAsync recupera a entidade selecionada e, em seguida, chama o método Remove para definir o status da entidade como Deleted. Quando SaveChanges é chamado, um comando SQL DELETE é gerado. Se Remove falhar:

  • A exceção de banco de dados é capturada.
  • O método OnGetAsync das páginas Excluir é chamado com saveChangesError=true.

Adicionar uma mensagem de erro a Excluir Razor Page (Pages/Students/Delete.cshtml):

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

Execute o aplicativo e exclua um aluno para testar a página Excluir.

Próximas etapas