Compartir a través de


Resolución de identidad en EF Core

Una instancia de DbContext solo puede realizar un seguimiento de una instancia de entidad con un valor de clave principal determinado. Esto significa que se deben resolver varias instancias de una entidad con el mismo valor de clave en una sola instancia. Esto se denomina "resolución de identidad". La resolución de identidad garantiza que Entity Framework Core (EF Core) realice un seguimiento de un grafo coherente sin ambigüedades sobre las relaciones o los valores de propiedad de las entidades.

Sugerencia

En este documento se da por supuesto que se comprenden los estados de entidad y los conceptos básicos del seguimiento de cambios de EF Core. Consulte Herramienta de seguimiento de cambios en EF Core para obtener más información sobre estos temas.

Sugerencia

Puede ejecutar y depurar en todo el código de este documento descargando el código de ejemplo de GitHub.

Introducción

El siguiente código consulta una entidad y luego intenta adjuntar una instancia diferente con el mismo valor de clave principal:

using var context = new BlogsContext();

var blogA = context.Blogs.Single(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };

try
{
    context.Update(blogB); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

La ejecución de este código produce la siguiente excepción:

System.InvalidOperationException: no se puede realizar un seguimiento de la instancia del tipo de entidad "Blog" porque ya se está realizando un seguimiento de otra instancia con el valor de clave "{Id: 1}". Al adjuntar entidades existentes, asegúrese de que solo se adjunte una instancia de entidad con un valor de clave determinado.

EF Core requiere una sola instancia por las razones siguientes:

  • Los valores de propiedad pueden ser diferentes entre varias instancias. Al actualizar la base de datos, EF Core debe saber qué valores de propiedad se van a usar.
  • Las relaciones con otras entidades pueden ser diferentes entre varias instancias. Por ejemplo, "blogA" puede estar relacionado con una colección de entradas diferente a la de "blogB".

La excepción anterior se encuentra normalmente en estas situaciones:

  • Al intentar actualizar una entidad.
  • Al intentar realizar un seguimiento de un grafo serializado de entidades.
  • Cuando no se puede establecer un valor de clave que no se genera automáticamente.
  • Al volver a usar una instancia de DbContext para varias unidades de trabajo.

Cada una de estas situaciones se describe en las secciones siguientes.

Actualización de una entidad

Hay varios enfoques diferentes para actualizar una entidad con nuevos valores, como se describe en Herramienta de seguimiento de cambios en EF Core y Seguimiento explícito de entidades. Estos enfoques se describen a continuación en el contexto de la resolución de identidad. Un punto importante a tener en cuenta es que cada uno de los enfoques usa una consulta o una llamada a Update o Attach, pero nunca a ambos.

Llamada a Update

A menudo, la entidad que se va a actualizar no procede de una consulta en la instancia de DbContext que vamos a usar para SaveChanges. Por ejemplo, en una aplicación web, se puede crear una instancia de entidad a partir de la información de una solicitud POST. La manera más sencilla de hacerlo es usar DbContext.Update o DbSet<TEntity>.Update. Por ejemplo:

public static void UpdateFromHttpPost1(Blog blog)
{
    using var context = new BlogsContext();

    context.Update(blog);

    context.SaveChanges();
}

En este caso:

  • Solo se crea una sola instancia de la entidad.
  • La instancia de entidad no se consulta desde la base de datos como parte de la actualización.
  • Todos los valores de propiedad se actualizarán en la base de datos, independientemente de si realmente han cambiado o no.
  • Se realiza un recorrido de ida y vuelta en la base de datos.

Consulta y, luego, aplicación de los cambios

Normalmente no se sabe qué valores de propiedad han cambiado realmente cuando se crea una entidad a partir de la información de una solicitud POST o similar. A menudo es correcto actualizar todos los valores de la base de datos, como se hace en el ejemplo anterior. Sin embargo, si la aplicación controla muchas entidades y solo un pequeño número de ellas tienen cambios reales, puede ser útil limitar las actualizaciones enviadas. Esto se puede lograr ejecutando una consulta para realizar un seguimiento de las entidades tal y como existen actualmente en la base de datos y, después, aplicando cambios a esas entidades con seguimiento. Por ejemplo:

public static void UpdateFromHttpPost2(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    trackedBlog.Name = blog.Name;
    trackedBlog.Summary = blog.Summary;

    context.SaveChanges();
}

En este caso:

  • Solo se realiza un seguimiento de una sola instancia de la entidad, la que la consulta Find devuelve de la base de datos.
  • Update, Attach, etc. ya no se usan.
  • Solo se actualizan en la base de datos los valores de propiedad que han cambiado realmente.
  • Se realizan dos recorridos de ida y vuelta en la base de datos.

EF Core tiene algunos asistentes para transferir valores de propiedad como este. Por ejemplo, PropertyValues.SetValues copiará todos los valores del objeto especificado y los establecerá en el objeto con seguimiento:

public static void UpdateFromHttpPost3(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(blog);

    context.SaveChanges();
}

SetValues acepta varios tipos de objeto, incluidos los objetos de transferencia de datos (DTO) con nombres de propiedad que coinciden con las propiedades del tipo de entidad. Por ejemplo:

public static void UpdateFromHttpPost4(BlogDto dto)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(dto.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(dto);

    context.SaveChanges();
}

O bien un diccionario con entradas de nombre y valor para los valores de propiedad:

public static void UpdateFromHttpPost5(Dictionary<string, object> propertyValues)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(propertyValues["Id"]);

    context.Entry(trackedBlog).CurrentValues.SetValues(propertyValues);

    context.SaveChanges();
}

Consulte Acceso a entidades sometidas a seguimiento para obtener más información sobre cómo trabajar con valores de propiedad como este.

Uso de los valores originales

Hasta ahora, cada enfoque ha ejecutado una consulta antes de realizar la actualización, o ha actualizado todos los valores de propiedad independientemente de si han cambiado o no. Para actualizar solo los valores que han cambiado sin realizar la consulta como parte de la actualización, se requiere información específica sobre qué valores de propiedad han cambiado. Una manera común de obtener esta información es devolver los valores actuales y originales de HTTP Post o similares. Por ejemplo:

public static void UpdateFromHttpPost6(Blog blog, Dictionary<string, object> originalValues)
{
    using var context = new BlogsContext();

    context.Attach(blog);
    context.Entry(blog).OriginalValues.SetValues(originalValues);

    context.SaveChanges();
}

En este código, se adjunta primero la entidad con valores modificados. Esto hace que EF Core realice un seguimiento de la entidad en el estado Unchanged; es decir, sin valores de propiedad marcados como modificados. Después se aplica el diccionario de valores originales a esa entidad con seguimiento. Esto marcará como modificadas las propiedades con diferentes valores actuales y originales. Las propiedades que tienen los mismos valores actuales y originales no se marcarán como modificadas.

En este caso:

  • Solo se realiza un seguimiento de una sola instancia de la entidad mediante Attach.
  • La instancia de entidad no se consulta desde la base de datos como parte de la actualización.
  • La aplicación de los valores originales garantiza que solo se actualicen en la base de datos los valores de propiedad que han cambiado realmente.
  • Se realiza un recorrido de ida y vuelta en la base de datos.

Al igual que con los ejemplos de la sección anterior, no es necesario pasar los valores originales como diccionario; tuna instancia de entidad o un objeto de transferencia de datos (DTO) también funcionarán.

Sugerencia

Aunque este enfoque tiene características interesantes, requiere enviar los valores originales de la entidad hacia y desde el cliente web. Considere detenidamente si esta complejidad adicional merece la pena; para muchas aplicaciones, resulta más práctico usar uno de los enfoques más sencillos.

Adjunción de un grafo serializado

EF Core funciona con grafos de entidades conectadas a través de claves externas y propiedades de navegación, como se describe en Cambio de claves externas y navegaciones. Si estos grafos se crean fuera de EF Core desde, por ejemplo, un archivo JSON, pueden tener varias instancias de la misma entidad. Estos duplicados deben resolverse en instancias únicas antes de que se pueda realizar el seguimiento del grafo.

Grafos sin duplicados

Antes de continuar, es importante reconocer que:

  • Los serializadores suelen tener opciones para gestionar los bucles y las instancias duplicadas en el grafo.
  • La elección del objeto usado como raíz del grafo a menudo puede ayudar a reducir o quitar duplicados.

Si es posible, use las opciones de serialización y elija raíces que no produzcan duplicados. Por ejemplo, el código siguiente usa Json.NET para serializar una lista de blogs cada uno con sus publicaciones asociadas:

using var context = new BlogsContext();

var blogs = context.Blogs.Include(e => e.Posts).ToList();

var serialized = JsonConvert.SerializeObject(
    blogs,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

El código JSON generado a partir de este código es el siguiente:

[
  {
    "Id": 1,
    "Name": ".NET Blog",
    "Summary": "Posts about .NET",
    "Posts": [
      {
        "Id": 1,
        "Title": "Announcing the Release of EF Core 5.0",
        "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
        "BlogId": 1
      },
      {
        "Id": 2,
        "Title": "Announcing F# 5",
        "Content": "F# 5 is the latest version of F#, the functional programming language...",
        "BlogId": 1
      }
    ]
  },
  {
    "Id": 2,
    "Name": "Visual Studio Blog",
    "Summary": "Posts about Visual Studio",
    "Posts": [
      {
        "Id": 3,
        "Title": "Disassembly improvements for optimized managed debugging",
        "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
        "BlogId": 2
      },
      {
        "Id": 4,
        "Title": "Database Profiling with Visual Studio",
        "Content": "Examine when database queries were executed and measure how long the take using...",
        "BlogId": 2
      }
    ]
  }
]

Tenga en cuenta que no hay blogs o publicaciones duplicados en el código JSON. Esto significa que las llamadas simples a Update funcionarán para actualizar estas entidades en la base de datos:

public static void UpdateBlogsFromJson(string json)
{
    using var context = new BlogsContext();

    var blogs = JsonConvert.DeserializeObject<List<Blog>>(json);

    foreach (var blog in blogs)
    {
        context.Update(blog);
    }

    context.SaveChanges();
}

Gestión de duplicados

El código del ejemplo anterior serializó cada blog con sus publicaciones asociadas. Si se cambia para serializar cada publicación con su blog asociado, los duplicados se introducen en el código JSON serializado. Por ejemplo:

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

El código JSON serializado tiene ahora el siguiente aspecto:

[
  {
    "Id": 1,
    "Title": "Announcing the Release of EF Core 5.0",
    "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 2,
          "Title": "Announcing F# 5",
          "Content": "F# 5 is the latest version of F#, the functional programming language...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 2,
    "Title": "Announcing F# 5",
    "Content": "F# 5 is the latest version of F#, the functional programming language...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 1,
          "Title": "Announcing the Release of EF Core 5.0",
          "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 3,
    "Title": "Disassembly improvements for optimized managed debugging",
    "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 4,
          "Title": "Database Profiling with Visual Studio",
          "Content": "Examine when database queries were executed and measure how long the take using...",
          "BlogId": 2
        }
      ]
    }
  },
  {
    "Id": 4,
    "Title": "Database Profiling with Visual Studio",
    "Content": "Examine when database queries were executed and measure how long the take using...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 3,
          "Title": "Disassembly improvements for optimized managed debugging",
          "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
          "BlogId": 2
        }
      ]
    }
  }
]

Observe que el grafo ahora incluye varias instancias de Blog con el mismo valor de clave, así como varias instancias de Post con el mismo valor de clave. Si se intenta realizar un seguimiento de este grafo como se hizo en el ejemplo anterior, se producirá lo siguiente:

System.InvalidOperationException: no se puede realizar un seguimiento de la instancia del tipo de entidad "Post" porque ya se está realizando un seguimiento de otra instancia con el valor de clave "{Id: 2}". Al adjuntar entidades existentes, asegúrese de que solo se adjunte una instancia de entidad con un valor de clave determinado.

Hay dos maneras de corregirlo:

  • Usar opciones de serialización de JSON que conserven las referencias.
  • Llevar a cabo la resolución de identidad mientras se realiza el seguimiento del grafo.

Conservación de las referencias

Json.NET proporciona la opción PreserveReferencesHandling para ello. Por ejemplo:

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.All, Formatting = Formatting.Indented
    });

El código JSON resultante ahora tiene el siguiente aspecto:

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "Id": 1,
      "Title": "Announcing the Release of EF Core 5.0",
      "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
      "BlogId": 1,
      "Blog": {
        "$id": "3",
        "Id": 1,
        "Name": ".NET Blog",
        "Summary": "Posts about .NET",
        "Posts": [
          {
            "$ref": "2"
          },
          {
            "$id": "4",
            "Id": 2,
            "Title": "Announcing F# 5",
            "Content": "F# 5 is the latest version of F#, the functional programming language...",
            "BlogId": 1,
            "Blog": {
              "$ref": "3"
            }
          }
        ]
      }
    },
    {
      "$ref": "4"
    },
    {
      "$id": "5",
      "Id": 3,
      "Title": "Disassembly improvements for optimized managed debugging",
      "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
      "BlogId": 2,
      "Blog": {
        "$id": "6",
        "Id": 2,
        "Name": "Visual Studio Blog",
        "Summary": "Posts about Visual Studio",
        "Posts": [
          {
            "$ref": "5"
          },
          {
            "$id": "7",
            "Id": 4,
            "Title": "Database Profiling with Visual Studio",
            "Content": "Examine when database queries were executed and measure how long the take using...",
            "BlogId": 2,
            "Blog": {
              "$ref": "6"
            }
          }
        ]
      }
    },
    {
      "$ref": "7"
    }
  ]
}

Tenga en cuenta que este código JSON ha reemplazado los duplicados por referencias como "$ref": "5", que hacen referencia a la instancia ya existente en el grafo. Se puede volver a realizar el seguimiento de este grafo mediante llamadas simples a Update, como se muestra más arriba.

La compatibilidad con System.Text.Json en las bibliotecas de clases base (BCL) de .NET tiene una opción similar que genera el mismo resultado. Por ejemplo:

var serialized = JsonSerializer.Serialize(
    posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });

Resolución de duplicados

Si no es posible eliminar los duplicados en el proceso de serialización, ChangeTracker.TrackGraph proporciona una manera de hacerlo. TrackGraph funciona como Add, Attach y Update, excepto que genera una devolución de llamada para cada instancia de entidad antes de realizar el seguimiento. Esta devolución de llamada se puede usar para realizar un seguimiento de la entidad o ignorarla. Por ejemplo:

public static void UpdatePostsFromJsonWithIdentityResolution(string json)
{
    using var context = new BlogsContext();

    var posts = JsonConvert.DeserializeObject<List<Post>>(json);

    foreach (var post in posts)
    {
        context.ChangeTracker.TrackGraph(
            post, node =>
            {
                var keyValue = node.Entry.Property("Id").CurrentValue;
                var entityType = node.Entry.Metadata;

                var existingEntity = node.Entry.Context.ChangeTracker.Entries()
                    .FirstOrDefault(
                        e => Equals(e.Metadata, entityType)
                             && Equals(e.Property("Id").CurrentValue, keyValue));

                if (existingEntity == null)
                {
                    Console.WriteLine($"Tracking {entityType.DisplayName()} entity with key value {keyValue}");

                    node.Entry.State = EntityState.Modified;
                }
                else
                {
                    Console.WriteLine($"Discarding duplicate {entityType.DisplayName()} entity with key value {keyValue}");
                }
            });
    }

    context.SaveChanges();
}

Para cada entidad del grafo, este código hará lo siguiente:

  • Buscar el tipo de entidad y el valor de clave de la entidad
  • Buscar la entidad con esta clave en el seguimiento de cambios
    • Si se encuentra la entidad, no se realiza ninguna otra acción, ya que la entidad es un duplicado.
    • Si no se encuentra la entidad, se realiza un seguimiento estableciendo el estado en Modified

El resultado de la ejecución de este código es el siguiente:

Tracking EntityType: Post entity with key value 1
Tracking EntityType: Blog entity with key value 1
Tracking EntityType: Post entity with key value 2
Discarding duplicate EntityType: Post entity with key value 2
Tracking EntityType: Post entity with key value 3
Tracking EntityType: Blog entity with key value 2
Tracking EntityType: Post entity with key value 4
Discarding duplicate EntityType: Post entity with key value 4

Importante

Este código supone que todos los duplicados son idénticos. Esto hace que sea seguro elegir arbitrariamente uno de los duplicados para realizar un seguimiento y descartar los demás. Si los duplicados pueden diferir, el código tendrá que decidir cómo determinar cuál usar y cómo combinar los valores de propiedad y navegación.

Nota:

Para simplificar, este código supone que cada entidad tiene una propiedad de clave principal denominada Id. Esto se podría codificar en una interfaz o clase base abstracta. Como alternativa, la propiedad o las propiedades de clave principales se podrían obtener de los metadatos IEntityType, de modo que este código funcionaría con cualquier tipo de entidad.

Valores clave no establecidos

A menudo, los tipos de entidad se configuran para usar valores de clave generados automáticamente. Este es el valor predeterminado para las propiedades integer y GUID de las claves no compuestas. Sin embargo, si el tipo de entidad no está configurado para usar valores de clave generados automáticamente, se debe establecer un valor de clave explícito antes de realizar un seguimiento de la entidad. Por ejemplo, mediante el siguiente tipo de entidad de seguimiento.

public class Pet
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }
}

Considere un código que intenta realizar un seguimiento de dos nuevas instancias de entidad sin establecer valores clave:

using var context = new BlogsContext();

context.Add(new Pet { Name = "Smokey" });

try
{
    context.Add(new Pet { Name = "Clippy" }); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Este código producirá lo siguiente:

System.InvalidOperationException: no se puede realizar un seguimiento de la instancia del tipo de entidad "Pet" porque ya se está realizando un seguimiento de otra instancia con el valor de clave "{Id: 0}". Al adjuntar entidades existentes, asegúrese de que solo se adjunte una instancia de entidad con un valor de clave determinado.

La solución consiste en establecer valores de clave explícitamente o configurar la propiedad de clave para usar valores de clave generados. Consulte Generated Values (Valores generados) para obtener más información.

Uso excesivo de una única instancia DbContext

DbContext está diseñado para representar una unidad de trabajo de corta duración, como se describe en Duración, configuración e inicialización de DbContext y se explica con más detalle en Herramienta de seguimiento de cambios en EF Core. Si no se siguen estas instrucciones, será fácil encontrarse con situaciones en las que se intente realizar un seguimiento de varias instancias de la misma entidad. Los ejemplos comunes son:

  • Uso de la misma instancia de DbContext para configurar el estado de la prueba y luego ejecutarla. Esto suele dar lugar a que DbContext siga realizando un seguimiento de una instancia de entidad de la configuración de la prueba, y luego intente adjuntar una nueva instancia en la prueba propiamente dicha. En lugar de ello, use otra instancia de DbContext para configurar el estado de prueba y el código de prueba propiamente dicho.
  • Uso de una instancia de DbContext compartida en un repositorio o un código similar. En lugar de ello, asegúrese de que el repositorio use una sola instancia de DbContext para cada unidad de trabajo.

Resolución de identidad y consultas

La resolución de identidad se produce automáticamente cuando se realiza un seguimiento de las entidades a partir de una consulta. Esto significa que si ya se realiza un seguimiento de una instancia de entidad con un valor de clave determinado, se usa esta instancia de seguimiento existente en lugar de crear una nueva instancia. Esto tiene una consecuencia importante: si los datos han cambiado en la base de datos, esto no se reflejará en los resultados de la consulta. Esta es una buena razón para usar una nueva instancia de DbContext para cada unidad de trabajo, como se describe en Duración, configuración e inicialización de DbContext y se explica con más detalle en Herramienta de seguimiento de cambios en EF Core.

Importante

Es importante comprender que EF Core siempre ejecuta una consulta LINQ en un DbSet en la base de datos y solo devuelve resultados en función de lo que hay en la base de datos. Sin embargo, para una consulta de seguimiento, si ya se realiza un seguimiento de las entidades devueltas, se usan las instancias con seguimiento en lugar de crear instancias a partir de los datos de la base de datos.

Reload() o GetDatabaseValues() se pueden usar cuando es necesario actualizar las entidades de seguimiento con los datos más recientes de la base de datos. Consulte Acceso a entidades sometidas a seguimiento para obtener más información.

A diferencia de las consultas de seguimiento, las consultas sin seguimiento no llevan a cabo la resolución de identidad. Esto significa que las consultas sin seguimiento pueden devolver duplicados al igual que en el caso de serialización de JSON descrito anteriormente. Esto no suele ser un problema si los resultados de la consulta se van a serializar y enviar al cliente.

Sugerencia

No realice rutinariamente consultas sin seguimiento y luego adjunte las entidades devueltas al mismo contexto. Esto será más lento y más difícil de hacer que usar una consulta de seguimiento.

Las consultas sin seguimiento no realizan la resolución de identidad porque hacerlo afecta al rendimiento de la transmisión de un gran número de entidades de una consulta. Esto se debe a que la resolución de identidad requiere hacer un seguimiento de cada instancia devuelta para poder usarla en lugar de crear posteriormente un duplicado.

Las consultas sin seguimiento se pueden forzar para realizar la resolución de identidad mediante AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>). Luego, la consulta realizará un seguimiento de las instancias devueltas (sin realizar un seguimiento de ellas de la manera habitual) y garantizará que no se creen duplicados en los resultados de la consulta.

Invalidación de la igualdad de objetos

EF Core usa la igualdad de referencia al comparar instancias de entidad. Este es el caso aunque los tipos de entidad invaliden Object.Equals(Object) o cambien la igualdad de objetos. Sin embargo, hay una situación en la que la invalidación de la igualdad puede afectar al comportamiento de EF Core: cuando las navegaciones de recolección usan la igualdad invalidada en lugar de la igualdad de referencia y, por lo tanto, notifican varias instancias como la misma.

Por este motivo, se recomienda evitar la invalidación de la igualdad de entidades. Si se usa, asegúrese de crear navegaciones de recolección que fuercen la igualdad de referencia. Por ejemplo, cree un comparador de igualdad que use la igualdad de referencia:

public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
    private ReferenceEqualityComparer()
    {
    }

    public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer();

    bool IEqualityComparer<object>.Equals(object x, object y) => x == y;

    int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}

(A partir de .NET 5, esto se incluye en las BCL como ReferenceEqualityComparer).

Este comparador se puede usar luego al crear navegaciones de recolección. Por ejemplo:

public ICollection<Order> Orders { get; set; }
    = new HashSet<Order>(ReferenceEqualityComparer.Instance);

Comparación de propiedades de clave

Además de las comparaciones de igualdad, también es necesario ordenar los valores de clave. Esto es importante para evitar interbloqueos al actualizar varias entidades en una sola llamada a SaveChanges. Todos los tipos usados para las propiedades de clave principales, alternativas o externas, así como para los usados para índices únicos, deben implementar IComparable<T> y IEquatable<T>. Los tipos que se usan normalmente como claves (int, Guid, string, etc.) ya admiten estas interfaces. Los tipos de clave personalizados pueden agregar estas interfaces.