Manipulando simultaneidade com o Entity Framework em um aplicativo MVC ASP.NET (7 de 10)

por Tom Dykstra

O aplicativo Web de exemplo da Contoso University demonstra como criar ASP.NET aplicativos MVC 4 usando o Entity Framework 5 Code First e o Visual Studio 2012. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial da série.

Observação

Se você tiver um problema, não poderá resolve, baixe o capítulo concluído e tente reproduzir o problema. Geralmente, você pode encontrar a solução para o problema comparando seu código com o código concluído. Para obter alguns erros comuns e como resolvê-los, consulte Erros e soluções alternativas.

Nos dois tutoriais anteriores, você trabalhou com dados relacionados. Este tutorial mostra como lidar com a simultaneidade. Você criará páginas da Web que funcionam com a Department entidade e as páginas que editam e excluem Department entidades lidarão com erros de simultaneidade. As ilustrações a seguir mostram as páginas Índice e Exclusão, incluindo algumas mensagens exibidas se ocorrer um conflito de simultaneidade.

Captura de tela que mostra a página Departamentos da Universidade contoso antes das edições.

A captura de tela mostra a página Universidade com uma mensagem que explica que a operação foi cancelada porque o valor foi alterado por outro usuário.

Conflitos de simultaneidade

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

Simultaneidade pessimista (bloqueio)

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

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

Simultaneidade otimista

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

Changing_English_dept_budget_to_100000

Antes de João clicar em Salvar, Jane executa a mesma página e altera o campo Data de Início de 01/09/2007 para 8/8/2013.

Changing_English_dept_start_date_to_1999

João clica em Salvar primeiro e vê sua alteração quando o navegador retorna à página Índice e, em seguida, Jane clica em Salvar. O que acontece em seguida é determinado pela forma como você lida com conflitos de simultaneidade. Algumas das opções incluem o seguinte:

  • Controle qual propriedade um usuário modificou e atualize apenas as colunas correspondentes no banco de dados. No cenário de exemplo, nenhum dado é perdido, porque propriedades diferentes foram atualizadas pelos dois usuários. Na próxima vez que alguém navegar pelo departamento de inglês, ele verá as mudanças de John e Jane , uma data de início de 8/8/2013 e um orçamento de Zero dólares.

    Esse método de atualização pode reduzir a quantidade de conflitos que podem resultar em perda de dados, mas ele não poderá evitar a perda de dados se forem feitas alterações concorrentes à mesma propriedade de uma entidade. Se o Entity Framework funciona dessa maneira depende de como o código de atualização é implementado. Geralmente, isso não é prático em um aplicativo Web, porque pode exigir que você mantenha grandes quantidades de estado para manter o controle de todos os valores de propriedade originais de uma entidade, bem como novos valores. Manter grandes quantidades de estado pode afetar o desempenho do aplicativo porque ele requer recursos do servidor ou deve ser incluído na própria página da Web (por exemplo, em campos ocultos).

  • Você pode deixar que a mudança de Jane substitua a mudança de John. Na próxima vez que alguém navegar pelo departamento de inglês, ele verá 8/8/2013 e o valor restaurado de US$ 350.000,00. Isso é chamado de um cenário O cliente vence ou O último vence. (Os valores do cliente têm precedência sobre o que está no armazenamento de dados.) Conforme observado na introdução a esta seção, se você não fizer nenhuma codificação para tratamento de simultaneidade, isso acontecerá automaticamente.

  • Você pode impedir que a alteração de Jane seja atualizada no banco de dados. Normalmente, você exibiria uma mensagem de erro, mostraria a ela o estado atual dos dados e permitiria que ela reaplicasse suas alterações se ela ainda quisesse fazê-las. Isso é chamado de um cenário O armazenamento vence. (Os valores do armazenamento de dados têm precedência sobre os valores enviados pelo cliente.) Você implementará o cenário O armazenamento vence neste tutorial. Esse método garante que nenhuma alteração é substituída sem que um usuário seja alertado sobre o que está acontecendo.

Detectando conflitos de simultaneidade

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

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

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

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

    Como na primeira opção, se alguma coisa na linha tiver sido alterada desde que a linha foi lida pela primeira vez, a Where cláusula não retornará uma linha para atualizar, o que o Entity Framework interpreta como um conflito de simultaneidade. Para tabelas de banco de dados que têm muitas colunas, essa abordagem pode resultar em cláusulas muito grandes Where e pode exigir que você mantenha grandes quantidades de estado. Conforme observado anteriormente, manter grandes quantidades de estado pode afetar o desempenho do aplicativo porque ele requer recursos do servidor ou deve ser incluído na própria página da Web. Portanto, essa abordagem geralmente não é recomendada e não é o método usado neste tutorial.

    Se você quiser implementar essa abordagem para simultaneidade, deverá marcar todas as propriedades de chave não primária na entidade para a qual deseja acompanhar a simultaneidade adicionando o atributo ConcurrencyCheck a elas. Essa alteração permite que o Entity Framework inclua todas as colunas na cláusula SQL WHERE de UPDATE instruções.

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

Adicionar uma propriedade de simultaneidade otimista à entidade department

Em Models\Department.cs, adicione uma propriedade de acompanhamento chamada RowVersion:

public class Department
{
    public int DepartmentID { get; set; }

    [StringLength(50, MinimumLength = 3)]
    public string Name { get; set; }

    [DataType(DataType.Currency)]
    [Column(TypeName = "money")]
    public decimal Budget { get; set; }

    [DataType(DataType.Date)]
    public DateTime StartDate { get; set; }

    [Display(Name = "Administrator")]
    public int? InstructorID { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }

    public virtual Instructor Administrator { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

O atributo Timestamp especifica que essa coluna será incluída na Where cláusula de Update comandos e Delete enviados ao banco de dados. O atributo é chamado timestamp porque as versões anteriores do SQL Server usaram um tipo de dados de carimbo de data/hora SQL antes da rowversion do SQL substituí-lo. O tipo .Net para rowversion é uma matriz de bytes. Se preferir usar a API fluente, você poderá usar o método IsConcurrencyToken para especificar a propriedade de acompanhamento, conforme mostrado no exemplo a seguir:

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

Consulte o problema do GitHub Substituir IsConcurrencyToken por IsRowVersion.

Adicionando uma propriedade, você alterou o modelo de banco de dados e, portanto, precisa fazer outra migração. No PMC (Console do Gerenciador de Pacotes), Insira os seguintes comandos:

Add-Migration RowVersion
Update-Database

Criar um controlador de departamento

Crie um Department controlador e exibições da mesma maneira que você fez com os outros controladores, usando as seguintes configurações:

Add_Controller_dialog_box_for_Department_controller

Em Controllers\DepartmentController.cs, adicione uma using instrução:

using System.Data.Entity.Infrastructure;

Altere "LastName" para "FullName" em todos os lugares neste arquivo (quatro ocorrências) para que as listas suspensas do administrador do departamento contenham o nome completo do instrutor em vez de apenas o sobrenome.

ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");

Substitua o código existente para o HttpPostEdit método pelo seguinte código:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
    Department department)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(department).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DbUpdateConcurrencyException ex)
   {
      var entry = ex.Entries.Single();
      var clientValues = (Department)entry.Entity;
      var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

      if (databaseValues.Name != clientValues.Name)
         ModelState.AddModelError("Name", "Current value: "
             + databaseValues.Name);
      if (databaseValues.Budget != clientValues.Budget)
         ModelState.AddModelError("Budget", "Current value: "
             + String.Format("{0:c}", databaseValues.Budget));
      if (databaseValues.StartDate != clientValues.StartDate)
         ModelState.AddModelError("StartDate", "Current value: "
             + String.Format("{0:d}", databaseValues.StartDate));
      if (databaseValues.InstructorID != clientValues.InstructorID)
         ModelState.AddModelError("InstructorID", "Current value: "
             + db.Instructors.Find(databaseValues.InstructorID).FullName);
      ModelState.AddModelError(string.Empty, "The record you attempted to edit "
          + "was modified by another user after you got the original value. The "
          + "edit operation was canceled and the current values in the database "
          + "have been displayed. If you still want to edit this record, click "
          + "the Save button again. Otherwise click the Back to List hyperlink.");
      department.RowVersion = databaseValues.RowVersion;
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
      ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
   }

   ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
   return View(department);
}

A exibição armazenará o valor original RowVersion em um campo oculto. Quando o associador de modelo cria a department instância, esse objeto terá o valor da propriedade original RowVersion e os novos valores para as outras propriedades, conforme inserido pelo usuário na página Editar. Em seguida, quando o Entity Framework cria um comando SQL UPDATE , esse comando incluirá uma WHERE cláusula que procura uma linha que tenha o valor original RowVersion .

Se nenhuma linha for afetada pelo UPDATE comando (nenhuma linha tem o valor original RowVersion ), o Entity Framework gerará uma DbUpdateConcurrencyException exceção e o código no catch bloco obterá a entidade afetada Department do objeto de exceção. Essa entidade tem os valores lidos do banco de dados e os novos valores inseridos pelo usuário:

var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();

Em seguida, o código adiciona uma mensagem de erro personalizada para cada coluna que tem valores de banco de dados diferentes do que o usuário inseriu na página Editar:

if (databaseValues.Name != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

Uma mensagem de erro mais longa explica o que aconteceu e o que fazer sobre isso:

ModelState.AddModelError(string.Empty, "The record you attempted to edit "
    + "was modified by another user after you got the original value. The"
    + "edit operation was canceled and the current values in the database "
    + "have been displayed. If you still want to edit this record, click "
    + "the Save button again. Otherwise click the Back to List hyperlink.");

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

Em Views\Department\Edit.cshtml, adicione um campo oculto para salvar o valor da RowVersion propriedade, seguindo imediatamente o campo oculto para a DepartmentID propriedade :

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>

Em Views\Department\Index.cshtml, substitua o código existente pelo código a seguir para mover links de linha para a esquerda e alterar o título da página e os títulos de coluna a serem exibidos FullName em vez de LastName na coluna Administrador :

@model IEnumerable<ContosoUniversity.Models.Department>

@{
    ViewBag.Title = "Departments";
}

<h2>Departments</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Name</th>
        <th>Budget</th>
        <th>Start Date</th>
        <th>Administrator</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Budget)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.StartDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Administrator.FullName)
        </td>
    </tr>
}

</table>

Testando o tratamento de simultaneidade otimista

Execute o site e clique em Departamentos:

Captura de tela que mostra a página Departamentos da Universidade contoso.

Clique com o botão direito do mouse no hiperlink Editar para Kim Abercrombie e selecione Abrir na nova guia e clique no hiperlink Editar para Kim Abercrombie. As duas janelas exibem as mesmas informações.

Department_Edit_page_before_changes

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

Department_Edit_page_1_after_change

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

Departments_Index_page_after_first_budget_edit

Altere qualquer campo na segunda janela do navegador e clique em Salvar.

Department_Edit_page_2_after_change

Clique em Salvar na segunda janela do navegador. Você verá uma mensagem de erro:

A captura de tela mostra a página Universidade com uma mensagem de erro, pronta para o usuário selecionar Salvar novamente.

Clique em Salvar novamente. O valor inserido no segundo navegador é salvo junto com o valor original dos dados alterados no primeiro navegador. Você verá os valores salvos quando a página Índice for exibida.

Department_Index_page_with_change_from_second_browser

Atualizando a página Excluir

Para a página Excluir, o Entity Framework detecta conflitos de simultaneidade causados pela edição por outra pessoa do departamento de maneira semelhante. Quando o HttpGetDelete método exibe a exibição de confirmação, a exibição inclui o valor original RowVersion em um campo oculto. Esse valor fica disponível para o HttpPostDelete método chamado quando o usuário confirma a exclusão. Quando o Entity Framework cria o comando SQL DELETE , ele inclui uma WHERE cláusula com o valor original RowVersion . Se o comando resultar em zero linhas afetadas (o que significa que a linha foi alterada após a exibição da página de confirmação Excluir), uma exceção de simultaneidade será gerada e o HttpGet Delete método será chamado com um sinalizador de erro definido true como para reexibir a página de confirmação com uma mensagem de erro. Também é possível que zero linhas tenham sido afetadas porque a linha foi excluída por outro usuário, portanto, nesse caso, uma mensagem de erro diferente é exibida.

Em DepartmentController.cs, substitua o HttpGetDelete método pelo seguinte código:

public ActionResult Delete(int id, bool? concurrencyError)
{
    Department department = db.Departments.Find(id);

    if (concurrencyError.GetValueOrDefault())
    {
        if (department == null)
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was deleted by another user after you got the original values. "
                + "Click the Back to List hyperlink.";
        }
        else
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was modified by another user after you got the original values. "
                + "The delete operation was canceled and the current values in the "
                + "database have been displayed. If you still want to delete this "
                + "record, click the Delete button again. Otherwise "
                + "click the Back to List hyperlink.";
        }
    }

    return View(department);
}

O método aceita um parâmetro opcional que indica se a página está sendo exibida novamente após um erro de simultaneidade. Se esse sinalizador for true, uma mensagem de erro será enviada para a exibição usando uma ViewBag propriedade .

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError=true } );
    }
    catch (DataException /* dex */)
    {
        //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
        ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}

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

public ActionResult DeleteConfirmed(int id)

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

public ActionResult Delete(Department department)

Você também alterou o nome do método de ação de DeleteConfirmed para Delete. O código scaffolded chamado método HttpPostDeleteDeleteConfirmed para dar ao HttpPost método uma assinatura exclusiva. (O CLR requer métodos sobrecarregados para ter parâmetros de método diferentes.) Agora que as assinaturas são exclusivas, você pode manter a convenção MVC e usar o mesmo nome para os HttpPost métodos e HttpGet excluir.

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

Em Views\Department\Delete.cshtml, substitua o código scaffolded pelo código a seguir que faz algumas alterações de formatação e adiciona um campo de mensagem de erro. As alterações são realçadas.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
    <legend>Department</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Budget)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Budget)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.StartDate)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.StartDate)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Administrator.FullName)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Administrator.FullName)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
   @Html.HiddenFor(model => model.DepartmentID)
    @Html.HiddenFor(model => model.RowVersion)
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

Esse código adiciona uma mensagem de erro entre os h2 títulos e h3 :

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

Ele substitui LastName por FullName no Administrator campo:

<div class="display-label">
    @Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
    @Html.DisplayFor(model => model.Administrator.FullName)
</div>

Por fim, ele adiciona campos ocultos para as DepartmentID propriedades e RowVersion após a instrução Html.BeginForm :

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

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

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

Department_Edit_page_after_change_before_delete

A página Índice confirma a alteração.

Departments_Index_page_after_budget_edit_before_delete

Na segunda janela, clique em Excluir.

Department_Delete_confirmation_page_before_concurrency_error

Você verá a mensagem de erro de simultaneidade e os valores de Departamento serão atualizados com o que está atualmente no banco de dados.

Department_Delete_confirmation_page_with_concurrency_error

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

Resumo

Isso conclui a introdução à manipulação de conflitos de simultaneidade. Para obter informações sobre outras maneiras de lidar com vários cenários de simultaneidade, consulte Padrões de simultaneidade otimistas e Trabalhando com valores de propriedade no blog da equipe do Entity Framework. O próximo tutorial mostra como implementar a herança tabela por hierarquia para as Instructor entidades e Student .

Links para outros recursos do Entity Framework podem ser encontrados no mapa de conteúdo de acesso a dados do ASP.NET.