Condividi tramite


Risoluzione delle identità in EF Core

Un DbContext oggetto può tenere traccia di un'istanza di entità con qualsiasi valore di chiave primaria specificato. Ciò significa che più istanze di un'entità con lo stesso valore di chiave devono essere risolte in una singola istanza. Questa operazione è denominata "risoluzione delle identità". La risoluzione delle identità garantisce che Entity Framework Core (EF Core) stia monitorando un grafico coerente senza ambiguità sulle relazioni o sui valori delle proprietà delle entità.

Suggerimento

Questo documento presuppone che gli stati dell'entità e le nozioni di base del rilevamento delle modifiche di EF Core siano compresi. Per altre informazioni su questi argomenti, vedere Rilevamento modifiche in EF Core.

Suggerimento

È possibile eseguire ed eseguire il debug in tutto il codice di questo documento scaricando il codice di esempio da GitHub.

Introduzione

Il codice seguente esegue una query per un'entità e quindi tenta di collegare un'istanza diversa con lo stesso valore di chiave primaria:

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

L'esecuzione di questo codice comporta l'eccezione seguente:

System.InvalidOperationException: impossibile tenere traccia dell'istanza del tipo di entità 'Blog' perché è già in corso il rilevamento di un'altra istanza con il valore della chiave '{Id: 1}'. Quando si collegano entità esistenti, assicurarsi che sia associata una sola istanza di entità con un valore di chiave specificato.

EF Core richiede una singola istanza perché:

  • I valori delle proprietà possono essere diversi tra più istanze. Quando si aggiorna il database, EF Core deve conoscere i valori delle proprietà da usare.
  • Le relazioni con altre entità possono essere diverse tra più istanze. Ad esempio, "blogA" può essere correlato a una raccolta diversa di post rispetto a "blogB".

L'eccezione precedente viene comunemente rilevata in queste situazioni:

  • Quando si tenta di aggiornare un'entità
  • Quando si tenta di tenere traccia di un grafico serializzato di entità
  • In caso di errore di impostazione di un valore di chiave non generato automaticamente
  • Quando si riutilizza un'istanza DbContext per più unità di lavoro

Ognuna di queste situazioni è descritta nelle sezioni seguenti.

Aggiornamento di un'entità

Esistono diversi approcci per aggiornare un'entità con nuovi valori, come descritto in Rilevamento modifiche in EF Core e Entità di rilevamento esplicito. Questi approcci sono descritti di seguito nel contesto della risoluzione delle identità. Un punto importante da notare è che ognuno degli approcci usa una query o una chiamata a uno di Update o , ma non entrambi.Attach

Chiamare l'aggiornamento

Spesso l'entità da aggiornare non proviene da una query in DbContext che verrà usata per SaveChanges. Ad esempio, in un'applicazione Web, un'istanza di entità può essere creata dalle informazioni in una richiesta POST. Il modo più semplice per gestire questa operazione consiste nell'usare DbContext.Update o DbSet<TEntity>.Update. Ad esempio:

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

    context.Update(blog);

    context.SaveChanges();
}

In questo caso:

  • Viene creata solo una singola istanza dell'entità.
  • L'istanza dell'entità non viene eseguita una query dal database come parte dell'esecuzione dell'aggiornamento.
  • Tutti i valori delle proprietà verranno aggiornati nel database, indipendentemente dal fatto che siano stati effettivamente modificati o meno.
  • Viene eseguito un round trip del database.

Eseguire quindi query per applicare le modifiche

In genere non è noto quali valori di proprietà sono stati effettivamente modificati quando un'entità viene creata da informazioni in una richiesta POST o simili. Spesso è consigliabile aggiornare solo tutti i valori nel database, come nell'esempio precedente. Tuttavia, se l'applicazione gestisce molte entità e solo un numero ridotto di tali entità presentano modifiche effettive, può essere utile limitare gli aggiornamenti inviati. A tale scopo, è possibile eseguire una query per tenere traccia delle entità esistenti nel database e quindi applicare le modifiche a queste entità rilevate. Ad esempio:

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

In questo caso:

  • Viene rilevata solo una singola istanza dell'entità; quello restituito dal database dalla Find query.
  • Update, Attache così via non vengono usati.
  • Nel database vengono aggiornati solo i valori delle proprietà effettivamente modificati.
  • Vengono effettuati due round trip del database.

EF Core include alcuni helper per il trasferimento di valori di proprietà come questo. Ad esempio, PropertyValues.SetValues copia tutti i valori dall'oggetto specificato e li imposta sull'oggetto rilevato:

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 accetta vari tipi di oggetto, inclusi gli oggetti di trasferimento dati (DTOs) con nomi di proprietà che corrispondono alle proprietà del tipo di entità. Ad esempio:

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

In alternativa, un dizionario con voci nome/valore per i valori delle proprietà:

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

Per altre informazioni sull'uso dei valori delle proprietà, vedere Accesso alle entità rilevate .

Usare i valori originali

Finora ogni approccio ha eseguito una query prima di eseguire l'aggiornamento o aggiornare tutti i valori delle proprietà indipendentemente dal fatto che siano stati modificati o meno. Per aggiornare solo i valori modificati senza eseguire query nell'ambito dell'aggiornamento, sono necessarie informazioni specifiche sui valori delle proprietà modificati. Un modo comune per ottenere queste informazioni consiste nell'inviare i valori correnti e originali nel post HTTP o in modo analogo. Ad esempio:

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

In questo codice l'entità con valori modificati viene prima associata. In questo modo EF Core tiene traccia dell'entità nello Unchanged stato, ovvero senza valori di proprietà contrassegnati come modificati. Il dizionario dei valori originali viene quindi applicato a questa entità rilevata. In questo modo verranno contrassegnate come proprietà modificate con valori correnti e originali diversi. Le proprietà con gli stessi valori correnti e originali non verranno contrassegnate come modificate.

In questo caso:

  • Viene rilevata solo una singola istanza dell'entità usando Attach.
  • L'istanza dell'entità non viene eseguita una query dal database come parte dell'esecuzione dell'aggiornamento.
  • L'applicazione dei valori originali garantisce che nel database vengano aggiornati solo i valori delle proprietà effettivamente modificati.
  • Viene eseguito un round trip del database.

Come per gli esempi nella sezione precedente, i valori originali non devono essere passati come dizionario; funzionerà anche un'istanza di entità o DTO.

Suggerimento

Sebbene questo approccio abbia caratteristiche accattivanti, richiede l'invio dei valori originali dell'entità da e verso il client Web. Valutare attentamente se questa complessità aggiuntiva vale i vantaggi; per molte applicazioni uno degli approcci più semplici è più pragmatico.

Collegamento di un grafico serializzato

EF Core funziona con grafici di entità connesse tramite chiavi esterne e proprietà di navigazione, come descritto in Modifica di chiavi esterne e spostamenti. Se questi grafici vengono creati all'esterno di EF Core usando, ad esempio, da un file JSON, possono avere più istanze della stessa entità. Questi duplicati devono essere risolti in singole istanze prima di poter tenere traccia del grafico.

Grafici senza duplicati

Prima di procedere, è importante riconoscere che:

  • I serializzatori hanno spesso opzioni per la gestione dei cicli e delle istanze duplicate nel grafico.
  • La scelta dell'oggetto usato come radice del grafo può spesso contribuire a ridurre o rimuovere duplicati.

Se possibile, usare le opzioni di serializzazione e scegliere le radici che non comportano duplicati. Ad esempio, il codice seguente usa Json.NET per serializzare un elenco di blog ognuno con i post associati:

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

Il codice JSON generato da questo codice è:

[
  {
    "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
      }
    ]
  }
]

Si noti che non sono presenti blog o post duplicati nel codice JSON. Ciò significa che le chiamate semplici a Update funzioneranno per aggiornare queste entità nel database:

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

Gestione dei duplicati

Il codice nell'esempio precedente ha serializzato ogni blog con i post associati. Se questa modifica viene modificata per serializzare ogni post con il blog associato, i duplicati vengono introdotti nel codice JSON serializzato. Ad esempio:

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

Il codice JSON serializzato è ora simile al seguente:

[
  {
    "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
        }
      ]
    }
  }
]

Si noti che il grafico include ora più istanze del blog con lo stesso valore di chiave, nonché più istanze di Post con lo stesso valore di chiave. Il tentativo di tenere traccia di questo grafo come nell'esempio precedente genererà:

System.InvalidOperationException: non è possibile tenere traccia dell'istanza del tipo di entità 'Post' perché è già in corso il rilevamento di un'altra istanza con il valore della chiave '{Id: 2}'. Quando si collegano entità esistenti, assicurarsi che sia associata una sola istanza di entità con un valore di chiave specificato.

È possibile risolvere questo problema in due modi:

  • Usare le opzioni di serializzazione JSON che mantengono i riferimenti
  • Eseguire la risoluzione delle identità durante il rilevamento del grafico

Mantenere i riferimenti

Json.NET offre l'opzione PreserveReferencesHandling per gestire questa operazione. Ad esempio:

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

Il codice JSON risultante è ora simile al seguente:

{
  "$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"
    }
  ]
}

Si noti che questo codice JSON ha sostituito i duplicati con riferimenti simili "$ref": "5" a quello che fanno riferimento all'istanza già esistente nel grafico. Questo grafico può essere monitorato di nuovo usando le semplici chiamate a Update, come illustrato in precedenza.

Il System.Text.Json supporto nelle librerie di classi di base .NET (BCL) ha un'opzione simile che produce lo stesso risultato. Ad esempio:

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

Risolvere i duplicati

Se non è possibile eliminare i duplicati nel processo di serializzazione, ChangeTracker.TrackGraph fornisce un modo per gestirli. TrackGraph funziona come Adde AttachUpdate , ad eccezione del fatto che genera un callback per ogni istanza di entità prima di monitorarlo. Questo callback può essere usato per tenere traccia dell'entità o ignorarla. Ad esempio:

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

Per ogni entità nel grafico, il codice seguente:

  • Trovare il tipo di entità e il valore della chiave dell'entità
  • Cercare l'entità con questa chiave nello strumento di rilevamento delle modifiche
    • Se l'entità viene trovata, non viene eseguita alcuna ulteriore azione perché l'entità è duplicata
    • Se l'entità non viene trovata, viene rilevata impostando lo stato su Modified

L'output dell'esecuzione di questo codice è:

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

Questo codice presuppone che tutti i duplicati siano identici. In questo modo è possibile scegliere arbitrariamente uno dei duplicati da tenere traccia mentre si eliminano gli altri. Se i duplicati possono differire, il codice dovrà decidere come determinarne uno da usare e come combinare i valori di proprietà e navigazione.

Nota

Per semplicità, questo codice presuppone che ogni entità abbia una proprietà chiave primaria denominata Id. Questa operazione può essere codificata in una classe o un'interfaccia di base astratta. In alternativa, la proprietà o le proprietà della chiave primaria possono essere ottenute dai IEntityType metadati in modo che questo codice funzioni con qualsiasi tipo di entità.

Errore durante l'impostazione dei valori di chiave

I tipi di entità vengono spesso configurati per l'uso di valori di chiave generati automaticamente. Si tratta dell'impostazione predefinita per le proprietà integer e GUID di chiavi non composite. Tuttavia, se il tipo di entità non è configurato per l'uso dei valori di chiave generati automaticamente, è necessario impostare un valore di chiave esplicito prima di tenere traccia dell'entità. Ad esempio, usando il tipo di entità seguente:

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

    public string Name { get; set; }
}

Prendere in considerazione il codice che tenta di tenere traccia di due nuove istanze di entità senza impostare i valori chiave:

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

Questo codice genererà:

System.InvalidOperationException: impossibile tenere traccia dell'istanza del tipo di entità 'Pet' perché è già in corso il rilevamento di un'altra istanza con il valore della chiave '{Id: 0}'. Quando si collegano entità esistenti, assicurarsi che sia associata una sola istanza di entità con un valore di chiave specificato.

La correzione per questa operazione consiste nell'impostare i valori di chiave in modo esplicito o configurare la proprietà della chiave per l'uso dei valori di chiave generati. Per altre informazioni, vedere Valori generati.

Overusing di una singola istanza DbContext

DbContextè progettato per rappresentare un'unità di lavoro di breve durata, come descritto in DbContext Initialization and Configuration e elaborato in Rilevamento modifiche in EF Core. Non seguendo queste indicazioni è facile eseguire situazioni in cui viene effettuato un tentativo di tenere traccia di più istanze della stessa entità. Esempi comuni:

  • Usando la stessa istanza DbContext per configurare lo stato di test e quindi eseguire il test. Questo comporta spesso il rilevamento di un'istanza di entità dal programma di installazione di test, mentre tenta di collegare una nuova istanza nel test corretto. Usare invece un'istanza DbContext diversa per configurare lo stato di test e il codice di test corretto.
  • Uso di un'istanza DbContext condivisa in un repository o in un codice simile. Assicurarsi invece che il repository usi una singola istanza DbContext per ogni unità di lavoro.

Risoluzione delle identità e query

La risoluzione delle identità viene eseguita automaticamente quando le entità vengono rilevate da una query. Ciò significa che se è già stata rilevata un'istanza di entità con un valore di chiave specificato, questa istanza rilevata esistente viene usata invece di creare una nuova istanza. Ciò ha una conseguenza importante: se i dati sono stati modificati nel database, questo non si rifletterà nei risultati della query. Questo è un buon motivo per usare una nuova istanza DbContext per ogni unità di lavoro, come descritto in DbContext Initialization and Configuration e elaborato in Rilevamento modifiche in EF Core.

Importante

È importante comprendere che EF Core esegue sempre una query LINQ su un DbSet sul database e restituisce solo i risultati in base a ciò che si trova nel database. Tuttavia, per una query di rilevamento, se le entità restituite sono già rilevate, le istanze rilevate vengono usate invece di creare istanze dai dati nel database.

Reload() oppure GetDatabaseValues() può essere usato quando è necessario aggiornare le entità rilevate con i dati più recenti del database. Per altre informazioni, vedere Accesso alle entità rilevate .

A differenza del rilevamento delle query, le query senza rilevamento non eseguono la risoluzione delle identità. Ciò significa che le query senza rilevamento possono restituire duplicati esattamente come nel caso di serializzazione JSON descritto in precedenza. Questo non è in genere un problema se i risultati della query verranno serializzati e inviati al client.

Suggerimento

Non eseguire regolarmente una query senza rilevamento e quindi allegare le entità restituite allo stesso contesto. Questo sarà più lento e più difficile da ottenere correttamente rispetto all'uso di una query di rilevamento.

Le query senza rilevamento non eseguono la risoluzione delle identità perché ciò influisce sulle prestazioni dello streaming di un numero elevato di entità da una query. Ciò è dovuto al fatto che la risoluzione delle identità richiede di tenere traccia di ogni istanza restituita in modo che possa essere usata invece di creare successivamente un duplicato.

Le query senza rilevamento possono essere forzate per eseguire la risoluzione delle identità tramite AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>). La query continuerà quindi a tenere traccia delle istanze restituite (senza tenerne traccia nel modo normale) e assicurarsi che non vengano creati duplicati nei risultati della query.

Override dell'uguaglianza degli oggetti

EF Core usa l'uguaglianza dei riferimenti durante il confronto delle istanze di entità. Questo è il caso anche se i tipi di entità eseguono l'override Object.Equals(Object) o modificano l'uguaglianza degli oggetti. Tuttavia, esiste un'unica posizione in cui l'override dell'uguaglianza può influire sul comportamento di EF Core: quando gli spostamenti della raccolta usano l'uguaglianza sottoposta a override anziché l'uguaglianza dei riferimenti e quindi segnalano più istanze dello stesso.

Per questo motivo è consigliabile evitare l'override dell'uguaglianza di entità. Se viene usato, assicurarsi di creare spostamenti di raccolta che forzano l'uguaglianza dei riferimenti. Ad esempio, creare un operatore di confronto di uguaglianza che usa l'uguaglianza dei riferimenti:

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 partire da .NET 5, questo è incluso nel BCL come ReferenceEqualityComparer.)

Questo operatore di confronto può quindi essere usato durante la creazione di spostamenti nella raccolta. Ad esempio:

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

Confronto delle proprietà chiave

Oltre ai confronti di uguaglianza, è necessario ordinare anche i valori chiave. Questo è importante per evitare deadlock durante l'aggiornamento di più entità in una singola chiamata a SaveChanges. Tutti i tipi usati per le proprietà di chiave primaria, alternativa o esterna, nonché per gli indici univoci, devono implementare IComparable<T> e IEquatable<T>. I tipi normalmente usati come chiavi (int, GUID, string e così via) supportano già queste interfacce. I tipi di chiave personalizzati possono aggiungere queste interfacce.