Compartir a través de


Manejar la simultaneidad con Entity Framework en una aplicación MVC de ASP.NET (7 de 10)

de Tom Dykstra

En la aplicación web de ejemplo Contoso University, se muestra cómo crear aplicaciones ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.

Nota:

Si se encuentra con un problema que no puede resolver, descargue el capítulo completado e intente reproducir el problema. Por lo general, puede encontrar la solución al problema comparando su código con el código completado. Para conocer algunos errores comunes y cómo resolverlos, consulte Errores y soluciones alternativas.

En los dos tutoriales anteriores, ha trabajado con datos relacionados. En este tutorial, se muestra cómo controlar la simultaneidad. Creará páginas web que funcionan con la entidad Department, y las páginas que editan y eliminan entidades Department controlarán los errores de simultaneidad. Las siguientes ilustraciones muestran las páginas Index y Delete, incluidos algunos mensajes que se muestran si se produce un conflicto de simultaneidad.

Screenshot shows the Contoso University Departments page before edits.

Screenshot shows the University page with a message that explains that the operation is canceled because the value has been changed by another user.

Conflictos de simultaneidad

Los conflictos de simultaneidad ocurren cuando un usuario muestra los datos de una entidad para editarlos y, después, otro usuario actualiza los datos de la misma entidad antes de que el primer cambio del usuario se escriba en la base de datos. Si no habilita la detección de este tipo de conflictos, quien actualice la base de datos en último lugar sobrescribe los cambios del otro usuario. En muchas aplicaciones, el riesgo es aceptable: si hay pocos usuarios o pocas actualizaciones, o si no es realmente importante si se sobrescriben algunos cambios, el costo de programación para la simultaneidad puede superar el beneficio obtenido. En ese caso, no tendrá que configurar la aplicación para que controle los conflictos de simultaneidad.

Simultaneidad pesimista (bloqueo)

Si la aplicación necesita evitar la pérdida accidental de datos en casos de simultaneidad, una manera de hacerlo es usar los bloqueos de base de datos. Esto se denomina simultaneidad pesimista. Por ejemplo, antes de leer una fila de una base de datos, solicita un bloqueo de solo lectura o para acceso de actualización. Si bloquea una fila para acceso de actualización, no se permite que ningún otro usuario bloquee la fila como solo lectura o para acceso de actualización, porque recibirían una copia de los datos que se están modificando. Si bloquea una fila para acceso de solo lectura, otras personas también pueden bloquearla para acceso de solo lectura pero no para actualización.

Administrar los bloqueos tiene desventajas. Puede ser bastante complicado de programar. Se necesita un número significativo de recursos de administración de base de datos, y puede provocar problemas de rendimiento a medida que aumenta el número de usuarios de una aplicación (es decir, no se escala bien). Por estos motivos, no todos los sistemas de administración de bases de datos admiten la simultaneidad pesimista. Entity Framework no proporciona ninguna compatibilidad integrada para ello y, en este tutorial, no se muestra cómo implementarla.

Simultaneidad optimista

La alternativa a la simultaneidad pesimista es la simultaneidad optimista. La simultaneidad optimista implica permitir que se produzcan conflictos de simultaneidad y reaccionar correctamente si ocurren. Por ejemplo, John visita la página Editar/Departamento y cambia la cantidad de Presupuesto para el departamento de inglés de $350 000,00 a $0,00.

Changing_English_dept_budget_to_100000

Antes de que John haga clic en Guardar, Jane visita la misma página y cambia el campo Fecha de inicio de 1/9/2007 a 8/8/2013.

Changing_English_dept_start_date_to_1999

John hace clic en Guardar primero y ve su cambio cuando el explorador vuelve a la página Índice y, a continuación, Jane hace clic en Guardar. Lo que sucede después viene determinado por cómo controla los conflictos de simultaneidad. Algunas de las opciones se exponen a continuación:

  • Puede realizar un seguimiento de la propiedad que ha modificado un usuario y actualizar solo las columnas correspondientes de la base de datos. En el escenario de ejemplo, no se perdería ningún dato porque los dos usuarios actualizaron diferentes propiedades. La próxima vez que un usuario examine el departamento de inglés, verá los cambios tanto de Jane como de John: una fecha de inicio de 8/8/2013 y un presupuesto de cero dólares.

    Este método de actualización puede reducir el número de conflictos que pueden dar lugar a una pérdida de datos, pero no puede evitar la pérdida de datos si se realizan cambios paralelos a la misma propiedad de una entidad. Si Entity Framework funciona de esta manera o no, depende de cómo implemente el código de actualización. A menudo no resulta práctico en una aplicación web, porque puede requerir mantener grandes cantidades de estado con el fin de realizar un seguimiento de todos los valores de propiedad originales de una entidad, así como los valores nuevos. Mantener grandes cantidades de estado puede afectar al rendimiento de la aplicación porque requiere recursos del servidor o se deben incluir en la propia página web (por ejemplo, en campos ocultos).

  • Puede permitir que los cambios de Jane sobrescriban los de John. La próxima vez que un usuario examine el departamento de inglés, verá 8/8/2013 y el valor de $350 000,00 restaurado. Esto se denomina un escenario de Prevalece el cliente o Prevalece el último. (Los valores del cliente tienen prioridad sobre lo que aparece en el almacén de datos). Como se mencionó en la introducción de esta sección, si no hace ninguna codificación para el control de la simultaneidad, se realizará automáticamente.

  • Puede impedir que el cambio de Jane se actualice en la base de datos. Por lo general, mostraría un mensaje de error, le mostraría el estado actual de los datos y le permitiría volver a aplicar sus cambios si todavía quiere realizarlos. Esto se denomina un escenario de Prevalece el almacén. (Los valores del almacén de datos tienen prioridad sobre los valores enviados por el cliente). En este tutorial implementará el escenario de Prevalece el almacén. Este método garantiza que ningún cambio se sobrescriba sin que se avise al usuario de lo que está sucediendo.

Detectar los conflictos de simultaneidad

Puede resolver conflictos mediante el control de excepciones OptimisticConcurrencyException que inicia Entity Framework. Para saber cuándo se producen dichas excepciones, Entity Framework debe ser capaz de detectar conflictos. Por lo tanto, debe configurar correctamente la base de datos y el modelo de datos. Algunas opciones para habilitar la detección de conflictos son las siguientes:

  • En la tabla de la base de datos, incluya una columna de seguimiento que pueda usarse para determinar si una fila ha cambiado. Después, puede configurar Entity Framework para incluir esa columna en la cláusula Where de los comandos Update o Delete de SQL.

    El tipo de datos de la columna de seguimiento suele ser rowversion. El valor rowversion es un número secuencial que se incrementa cada vez que se actualiza la fila. En un comando Update o Delete, la cláusula Where incluye el valor original de la columna de seguimiento (la versión de la fila original). Si otro usuario ha cambiado la fila que se está actualizando, el valor en la columna rowversion es diferente del valor original, por lo que la instrucción Update o Delete no puede encontrar la fila que se va a actualizar debido a la cláusula Where. Cuando Entity Framework encuentra que no se ha actualizado ninguna fila mediante el comando Update o Delete (es decir, cuando el número de filas afectadas es cero), lo interpreta como un conflicto de simultaneidad.

  • Configure Entity Framework para que incluya los valores originales de cada columna de la tabla en la cláusula Where de los comandos Update y Delete.

    Como se muestra en la primera opción, si algo en la fila ha cambiado desde que se leyó por primera vez, la cláusula Where no devolverá una fila para actualizar, lo cual Entity Framework interpreta como un conflicto de simultaneidad. Para las tablas de base de datos que tienen muchas columnas, este enfoque puede dar lugar a cláusulas Where muy grandes y puede requerir mantener grandes cantidades de estado. Como se mostró anteriormente, mantener grandes cantidades de estado puede afectar al rendimiento de la aplicación porque requiere recursos del servidor o se deben incluir en la propia página web. Por tanto, generalmente este enfoque no se recomienda y no es el método usado en este tutorial.

    Si quiere implementar este enfoque para la simultaneidad, tendrá que marcar todas las propiedades de clave no principal de la entidad de la que quiere realizar un seguimiento de simultaneidad agregándoles el atributo ConcurrencyCheck. Ese cambio permite que Entity Framework incluya todas las columnas en la cláusula WHERE de SQL de las instrucciones UPDATE.

En el resto de este tutorial agregará una propiedad de seguimiento rowversion para la entidad Department, creará un controlador y vistas, y comprobará que todo funciona correctamente.

Agregar una propiedad de simultaneidad optimista a la entidad Department

En Models/Department.cs, agregue una propiedad de seguimiento denominada 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; }
}

El atributo Timestamp especifica que esta columna se incluirá en la cláusula Where de los comandos Update y Delete enviados a la base de datos. El atributo se denomina Timestamp porque las versiones anteriores de SQL Server usaban un tipo de datos timestamp de SQL antes de que la rowversion de SQL la reemplazara. El tipo .NET de rowversion es una matriz de bytes. Si prefiere usar la API fluida, puede usar el método IsConcurrencyToken para especificar la propiedad de seguimiento, tal como se muestra en el ejemplo siguiente:

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

Consulte el problema de GitHub Reemplazar IsConcurrencyToken por IsRowVersion.

Al agregar una propiedad cambió el modelo de base de datos, por lo que necesita realizar otra migración. En la Consola del Administrador de paquetes (PMC), escriba los comandos siguientes:

Add-Migration RowVersion
Update-Database

Creación de un controlador de departamento

Cree un controlador Department y vistas de la misma manera que lo ha hecho con los demás controladores, con la siguiente configuración:

Add_Controller_dialog_box_for_Department_controller

En Controllers\DepartmentController.cs, agregue una instrucción using:

using System.Data.Entity.Infrastructure;

Cambie "LastName" a "FullName" en todas partes de este archivo (cuatro repeticiones) para que las listas desplegables del administrador del departamento contengan el nombre completo del instructor en lugar de solo el apellido.

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

En el método HttpPostEdit, sustituya el código existente por el siguiente 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);
}

La vista almacenará el valor original RowVersion en un campo oculto. Cuando el enlazador de modelos crea la instancia department, ese objeto tendrá el valor de propiedad original RowVersion y los nuevos valores para las otras propiedades, tal y como ha escrito el usuario en la página Editar. Cuando Entity Framework crea un comando UPDATE de SQL, ese comando incluirá una cláusula WHERE que comprueba si hay una fila que tenga el valor RowVersion original.

Si no hay filas afectadas por el comando UPDATE (ninguna fila tiene el valor original RowVersion), Entity Framework produce una excepción DbUpdateConcurrencyException y el código del bloque catch obtiene la entidad Department afectada del objeto de excepción. Esta entidad tiene los valores leídos de la base de datos y los nuevos valores especificados por el usuario:

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

Después, el código agrega un mensaje de error personalizado para cada columna que tenga valores de base de datos diferentes de lo que el usuario especificó en la página Editar:

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

Un mensaje de error más largo explica lo que ha ocurrido y qué hacer sobre él:

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 último, el código establece el valor RowVersion del objeto Department para el nuevo valor recuperado de la base de datos. Este nuevo valor RowVersion se almacenará en el campo oculto cuando se vuelva a mostrar la página Edit y, la próxima vez que el usuario haga clic en Save, solo se detectarán los errores de simultaneidad que se produzcan desde que se vuelva a mostrar la página Edit.

En Views\Department\Edit.cshtml, agregue un campo oculto para guardar el valor de propiedad RowVersion, inmediatamente después de un campo oculto para la propiedad DepartmentID:

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

En Views\Department\Index.cshtml, reemplace el código existente por el código siguiente para mover los vínculos de fila a la izquierda y cambie los encabezados de título y columna de página para mostrar FullName en lugar de LastName en la columna 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>

Prueba del control de simultaneidad optimista

Ejecute el sitio y haga clic en Departamentos:

Screenshot shows the Contoso University Departments page.

Haga clic con el botón derecho en el hipervínculo Editar de Kim Abercrombie y seleccione Abrir en nueva pestaña y, a continuación, haga clic en el hipervínculo Editar para Kim Abercrombie. Las dos ventanas muestran la misma información.

Department_Edit_page_before_changes

Cambie un campo en la primera ventana del explorador y haga clic en Guardar.

Department_Edit_page_1_after_change

El explorador muestra la página de índice con el valor modificado.

Departments_Index_page_after_first_budget_edit

Cambie cualquier campo de la segunda ventana del explorador y haga clic en Guardar.

Department_Edit_page_2_after_change

Haga clic en Guardar en la segunda ventana del explorador. Verá un mensaje de error:

Screenshot shows the University page with an error message, ready for the user to select Save again.

Vuelva a hacer clic en Save. El valor especificado en el segundo explorador se guarda junto con el valor original de los datos que cambia en el primer explorador. Verá los valores guardados cuando aparezca la página de índice.

Department_Index_page_with_change_from_second_browser

Actualización de la página Eliminar

Para la página Delete, Entity Framework detecta los conflictos de simultaneidad causados por una persona que edita el departamento de forma similar. Cuando el método HttpGetDelete muestra la vista de confirmación, la vista incluye el valor RowVersion original en un campo oculto. Dicho valor está entonces disponible para el método HttpPostDelete al que se llama cuando el usuario confirma la eliminación. Cuando Entity Framework crea el comando DELETE de SQL, incluye una cláusula WHERE con el valor original RowVersion. Si el comando tiene como resultado cero filas afectadas (es decir, la fila se cambió después de que se muestre la página de confirmación de eliminación), se produce una excepción de simultaneidad y el método HttpGet Delete se llama con una marca de error establecida en true para volver a mostrar la página de confirmación con un mensaje de error. También es posible que se vieran afectadas cero filas porque otro usuario eliminó la fila, por lo que en ese caso se muestra otro mensaje de error.

En DepartmentController.cs, reemplace el método HttpGetDelete por el código siguiente:

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

El método acepta un parámetro opcional que indica si la página volverá a aparecer después de un error de simultaneidad. Si esta marca es true, se envía un mensaje de error a la vista mediante una propiedad ViewBag.

Reemplace el código en el método HttpPostDelete (denominado DeleteConfirmed) con el código siguiente:

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

En el código al que se aplicó la técnica scaffolding que acaba de reemplazar, este método solo acepta un identificador de registro:

public ActionResult DeleteConfirmed(int id)

Ha cambiado este parámetro por una instancia de la entidad Department creada por el enlazador de modelos. Esto le proporciona acceso al valor de propiedad RowVersion además de la clave de registro.

public ActionResult Delete(Department department)

También ha cambiado el nombre del método de acción de DeleteConfirmed a Delete. El código al que se aplicó la técnica scaffolding asignó al método HttpPostDelete el nombre DeleteConfirmed para proporcionar al método HttpPost una firma única. (El CLR requiere métodos sobrecargados para tener parámetros de método diferentes). Ahora que las firmas son únicas, puede ceñirse a la convención MVC y usar el mismo nombre para los métodos de eliminación HttpPost y HttpGet.

Si se detecta un error de simultaneidad, el código vuelve a mostrar la página de confirmación de Delete y proporciona una marca que indica que se debería mostrar un mensaje de error de simultaneidad.

En Views\Department\Delete.cshtml, reemplace el código con scaffolding por el código siguiente que realiza algunos cambios de formato y agrega un campo de mensaje de error. Los cambios aparecen resaltados.

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

Este código agrega un mensaje de error entre los encabezados h2 y h3:

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

Reemplaza LastName por FullName en el campo Administrator:

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

Por último, agrega campos ocultos para las propiedades DepartmentID y RowVersion después de la instrucción Html.BeginForm:

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

Actualizar la página de Indice de Departamentos. Haga clic con el botón derecho en el hipervínculo Eliminar del departamento de inglés, seleccione Abrir en nueva ventana y, después, en la primera ventana, haga clic en el hipervínculo Editar del departamento de inglés.

En la primera ventana, cambie uno de los valores y haga clic en Save:

Department_Edit_page_after_change_before_delete

La página Índice confirma el cambio.

Departments_Index_page_after_budget_edit_before_delete

En la segunda ventana, haga clic en Eliminar.

Department_Delete_confirmation_page_before_concurrency_error

Verá el mensaje de error de simultaneidad y se actualizarán los valores de Department con lo que está actualmente en la base de datos.

Department_Delete_confirmation_page_with_concurrency_error

Si vuelve a hacer clic en Delete, se le redirigirá a la página de índice, que muestra que se ha eliminado el departamento.

Resumen

Con esto finaliza la introducción para el control de los conflictos de simultaneidad. Para obtener información sobre otras formas de controlar varios escenarios de simultaneidad, consulte Patrones de simultaneidad optimista y Trabajar con valores de propiedad en el blog del equipo de Entity Framework. El siguiente tutorial muestra cómo implementar la herencia de tabla por jerarquía para las entidades Instructor y Student.

En el mapa de contenido de acceso a datos de ASP.NET se pueden encontrar vínculos a otros recursos de Entity Framework.