Parte 2, Razor Pages com EF Core no ASP.NET Core – CRUD
Observação
Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Importante
Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.
Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
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
comFindAsync
. 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 paraDateTime
.
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.
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âmetroid
.- 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étodoSaveChanges
emite uma instruçãoINSERT
.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étodoSaveChanges
emite uma instruçãoUPDATE
.Deleted
: a entidade foi marcada para exclusão. O métodoSaveChanges
emite uma instruçãoDELETE
.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étodoOnGetAsync
.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 comsaveChangesError=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
comFindAsync
. 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 paraDateTime
.
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.
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âmetroid
.- 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étodoSaveChanges
emite uma instruçãoINSERT
.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étodoSaveChanges
emite uma instruçãoUPDATE
.Deleted
: a entidade foi marcada para exclusão. O métodoSaveChanges
emite uma instruçãoDELETE
.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étodoOnGetAsync
.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 comsaveChangesError=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
comFindAsync
. 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.
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âmetroid
.- 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étodoSaveChanges
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étodoSaveChanges
emite uma instrução UPDATE.Deleted
: a entidade foi marcada para exclusão. O métodoSaveChanges
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 comsaveChangesError=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.