Eventos
19 de nov., 23 - 21 de nov., 23
Participe de sessões online no Microsoft Ignite criadas para expandir suas habilidades e ajudá-lo a lidar com os problemas complexos de hoje.
Registrar agoraNão há mais suporte para esse navegador.
Atualize o Microsoft Edge para aproveitar os recursos, o suporte técnico e as atualizações de segurança mais recentes.
Observação
Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 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 a versão atual, consulte a versão .NET 9 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.
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.
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
.
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.
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.
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
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.Include
com FindAsync
. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync
será a melhor opção.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.
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();
}
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:
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
).Student.FirstMidName
. Não diferencia maiúsculas de minúsculas.Student
. Por exemplo, EnrollmentDate
é convertido para DateTime
.Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.
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.
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:
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");}
}
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
.Execute o aplicativo e teste-o criando e editando um aluno.
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.
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:
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:
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.
Neste tutorial, o código CRUD (criar, ler, atualizar e excluir) gerado por scaffolding é examinado e personalizado.
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.
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
.
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.
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.
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
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.Include
com FindAsync
. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync
será a melhor opção.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.
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();
}
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:
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
).Student.FirstMidName
. Não diferencia maiúsculas de minúsculas.Student
. Por exemplo, EnrollmentDate
é convertido para DateTime
.Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.
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.
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:
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");}
}
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
.Execute o aplicativo e teste-o criando e editando um aluno.
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.
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:
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:
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.
Neste tutorial, o código CRUD (criar, ler, atualizar e excluir) gerado por scaffolding é examinado e personalizado.
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.
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.
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.
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.
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
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.Include
com FindAsync
. Portanto, se forem necessários dados relacionados, FirstOrDefaultAsync
será a melhor opção.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.
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();
}
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:
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate
).Student.FirstMidName
. Não diferencia maiúsculas de minúsculas.Student
. Por exemplo, EnrollmentDate
deve ser convertido em DateTime.Execute o aplicativo e crie uma entidade de aluno para testar a página Criar.
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.
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
.
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
.Execute o aplicativo e teste-o criando e editando um aluno.
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.
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:
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.
Comentários do ASP.NET Core
O ASP.NET Core é um projeto código aberto. Selecione um link para fornecer comentários:
Eventos
19 de nov., 23 - 21 de nov., 23
Participe de sessões online no Microsoft Ignite criadas para expandir suas habilidades e ajudá-lo a lidar com os problemas complexos de hoje.
Registrar agora