Partager via


Résolution des identités dans EF Core

Une DbContext ne peut suivre qu’une seule instance d’entité avec une valeur de clé primaire donnée. Cela signifie que plusieurs instances d’une entité avec la même valeur de clé doivent être résolues en une seule instance. C’est ce qu’on appelle « résolution d’identité ». La résolution d’identité garantit que Entity Framework Core (EF Core) suit un graphique cohérent sans ambiguïté concernant les relations ou les valeurs de propriété des entités.

Conseil

Ce document suppose que les états d’entité et les principes de base du suivi des modifications EF Core sont compris. Pour plus d’informations sur ces rubriques, consultez Suivi des modifications dans EF Core.

Conseil

Vous pouvez exécuter et déboguer dans tout le code de ce document en téléchargeant l’exemple de code à partir de GitHub.

Présentation

Le code suivant interroge une entité, puis tente d’attacher une autre instance avec la même valeur de clé primaire :

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’exécution de ce code entraîne l’exception suivante :

System.InvalidOperationException : L’instance du type d’entité « Blog » ne peut pas être suivie, car une autre instance avec la valeur de clé « {Id : 1} » est déjà suivie. Lors de l’attachement d’entités existantes, vérifiez qu’une seule instance d’entité avec une valeur de clé donnée est attachée.

EF Core nécessite une seule instance, car :

  • Les valeurs de propriété peuvent être différentes entre plusieurs instances. Lors de la mise à jour de la base de données, EF Core doit savoir quelles valeurs de propriété utiliser.
  • Les relations avec d’autres entités peuvent être différentes entre plusieurs instances. Par exemple, « blogA » peut être lié à une collection de billets différente de « blogB ».

L’exception ci-dessus est généralement rencontrée dans ces situations :

  • Lors de la tentative de mise à jour d’une entité
  • Lors de la tentative de suivi d’un graphique sérialisé d’entités
  • En cas d’échec de définition d’une valeur de clé qui n’est pas générée automatiquement
  • Lors de la réutilisation d’une instance DbContext pour plusieurs unités de travail

Chacune de ces situations est abordée dans les sections suivantes.

Mise à jour d'une entité

Il existe plusieurs approches différentes pour mettre à jour une entité avec de nouvelles valeurs, comme décrit dans Suivi des modifications dans EF Core et Entités de suivi explicite. Ces approches sont décrites ci-dessous dans le contexte de la résolution d’identité. Un point important à remarquer est que chacune des approches utilise une requête ou un appel à l’un des Update ou Attach, mais jamais les deux.

Mise à jour des appels

Souvent, l’entité à mettre à jour ne provient pas d’une requête sur DbContext que nous allons utiliser pour SaveChanges. Par exemple, dans une application web, une instance d’entité peut être créée à partir des informations d’une requête POST. La façon la plus simple de gérer cela consiste à utiliser DbContext.Update ou DbSet<TEntity>.Update. Par exemple :

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

    context.Update(blog);

    context.SaveChanges();
}

Dans ce cas :

  • Une seule instance de l’entité est créée.
  • L’instance d’entité n’est pas interrogée à partir de la base de données dans le cadre de la mise à jour.
  • Toutes les valeurs de propriété sont mises à jour dans la base de données, qu’elles aient réellement changé ou non.
  • Un aller-retour de base de données est effectué.

Requête puis appliquer des modifications

En règle générale, il n’est pas connu les valeurs de propriété qui ont été réellement modifiées lorsqu’une entité est créée à partir d’informations dans une requête POST ou similaire. Il est souvent judicieux de mettre à jour toutes les valeurs de la base de données, comme nous l’avons fait dans l’exemple précédent. Toutefois, si l’application gère de nombreuses entités et qu’un petit nombre de ces entités ont des modifications réelles, il peut être utile de limiter les mises à jour envoyées. Pour ce faire, exécutez une requête pour suivre les entités telles qu’elles existent actuellement dans la base de données, puis en appliquant des modifications à ces entités suivies. Par exemple :

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

Dans ce cas :

  • Une seule instance de l’entité est suivie ; celui retourné à partir de la base de données par la requête Find.
  • Update, Attach, etc. ne sont pas utilisés .
  • Seules les valeurs de propriété qui ont réellement changé sont mises à jour dans la base de données.
  • Deux allers-retours de base de données sont effectués.

EF Core a quelques assistances pour transférer des valeurs de propriété comme celle-ci. Par exemple, PropertyValues.SetValues copie toutes les valeurs de l’objet donné et les définit sur l’objet suivi :

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 accepte différents types d’objets, y compris les objets de transfert de données (DTO) avec des noms de propriétés qui correspondent aux propriétés du type d’entité. Par exemple :

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

Ou un dictionnaire avec des entrées nom/valeur pour les valeurs de propriété :

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

Consultez Accès aux entités suivies pour plus d’informations sur l’utilisation des valeurs de propriété comme celle-ci.

Utiliser des valeurs d’origine

Jusqu’à présent, chaque approche a exécuté une requête avant d’effectuer la mise à jour ou mis à jour toutes les valeurs de propriété, qu’elles aient ou non changé. Pour mettre à jour uniquement les valeurs qui ont changé sans interroger dans le cadre de la mise à jour, vous devez disposer d’informations spécifiques sur les valeurs de propriété qui ont changé. Une façon courante d’obtenir ces informations consiste à renvoyer à la fois les valeurs actuelles et d’origine dans le billet HTTP ou similaires. Par exemple :

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

Dans ce code, l’entité avec des valeurs modifiées est d’abord attachée. EF Core effectue ainsi le suivi de l’entité dans l’état Unchanged ; autrement dit, sans valeurs de propriété marquées comme modifiées. Le dictionnaire de valeurs d’origine est ensuite appliqué à cette entité suivie. Cela marque les propriétés modifiées avec différentes valeurs actuelles et d’origine. Les propriétés qui ont les mêmes valeurs actuelles et d’origine ne sont pas marquées comme modifiées.

Dans ce cas :

  • Une seule instance de l’entité est suivie à l’aide de Attach.
  • L’instance d’entité n’est pas interrogée à partir de la base de données dans le cadre de la mise à jour.
  • L’application des valeurs d’origine garantit que seules les valeurs de propriété qui ont réellement changé sont mises à jour dans la base de données.
  • Un aller-retour de base de données est effectué.

Comme pour les exemples de la section précédente, les valeurs d’origine ne doivent pas être passées en tant que dictionnaire ; une instance d’entité ou un DTO fonctionne également.

Conseil

Bien que cette approche présente des caractéristiques attrayantes, elle nécessite l’envoi des valeurs d’origine de l’entité vers et depuis le client web. Déterminez soigneusement si cette complexité supplémentaire vaut les avantages ; pour de nombreuses applications, l’une des approches plus simples est plus pragmatique.

Attachement d’un graphique sérialisé

EF Core fonctionne avec des graphiques d’entités connectées via des clés étrangères et des propriétés de navigation, comme décrit dans Modification des clés étrangères et des navigations. Si ces graphiques sont créés en dehors d’EF Core à l’aide, par exemple, d’un fichier JSON, ils peuvent avoir plusieurs instances de la même entité. Ces doublons doivent être résolus en instances uniques avant que le graphique puisse être suivi.

Graphiques sans doublons

Avant d’aller plus loin, il est important de reconnaître que :

  • Les sérialiseurs ont souvent des options pour gérer les boucles et les instances dupliquées dans le graphique.
  • Le choix de l’objet utilisé comme racine du graphique peut souvent aider à réduire ou supprimer des doublons.

Si possible, utilisez des options de sérialisation et choisissez des racines qui n’entraînent pas de doublons. Par exemple, le code suivant utilise Json.NET pour sérialiser une liste de blogs chacun avec ses billets associés :

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

Le code JSON généré à partir de ce code est le suivant :

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

Notez qu’il n’y a pas de blogs ou de billets en double dans le JSON. Cela signifie que les appels simples à Update fonctionnent pour mettre à jour ces entités dans la base de données :

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

Gestion des doublons

Le code de l’exemple précédent a sérialisé chaque blog avec ses billets associés. S’il est modifié pour sérialiser chaque billet avec son blog associé, les doublons sont introduits dans le JSON sérialisé. Par exemple :

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

Le JSON sérialisé ressemble maintenant à ceci :

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

Notez que le graphique inclut désormais plusieurs instances de blog avec la même valeur de clé, ainsi que plusieurs instances post avec la même valeur de clé. La tentative de suivi de ce graphique comme nous l’avons fait dans l’exemple précédent lève :

System.InvalidOperationException : L’instance de type d’entité « Post » ne peut pas être suivie, car une autre instance avec la valeur de clé « {Id : 2} » est déjà suivie. Lors de l’attachement d’entités existantes, vérifiez qu’une seule instance d’entité avec une valeur de clé donnée est attachée.

Nous pouvons résoudre ce problème de deux manières :

  • Utiliser des options de sérialisation JSON qui conservent les références
  • Effectuer une résolution d’identité pendant le suivi du graphique

Préserver les références

Json.NET fournit l’option PreserveReferencesHandling pour gérer ce problème. Par exemple :

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

Le JSON obtenu ressemble maintenant à ceci :

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

Notez que ce JSON a remplacé les doublons par des références comme "$ref": "5" qui font référence à l’instance déjà existante dans le graphique. Ce graphique peut à nouveau être suivi à l’aide des appels simples pour Update, comme indiqué ci-dessus.

La prise en charge System.Text.Json dans les bibliothèques de classes de base .NET (BCL) a une option similaire qui produit le même résultat. Par exemple :

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

Résoudre les doublons

S’il n’est pas possible d’éliminer les doublons dans le processus de sérialisation, ChangeTracker.TrackGraph permet de gérer cela. TrackGraph fonctionne comme Add, Attach et Update , sauf qu’il génère un rappel pour chaque instance d’entité avant de le suivre. Ce rappel peut être utilisé pour suivre l’entité ou l’ignorer. Par exemple :

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

Pour chaque entité du graphique, ce code :

  • Rechercher le type d’entité et la valeur de clé de l’entité
  • Rechercher l’entité avec cette clé dans le suivi des modifications
    • Si l’entité est trouvée, aucune autre action n’est effectuée, car l’entité est un doublon
    • Si l’entité est introuvable, elle est suivie en définissant l’état sur Modified

La sortie de l’exécution de ce code est la suivante :

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

Important

Ce code suppose que tous les doublons sont identiques. Cela permet de choisir arbitrairement l’un des doublons à suivre tout en ignorant les autres. Si les doublons peuvent différer, le code doit décider comment déterminer celui à utiliser et comment combiner des valeurs de propriété et de navigation.

Remarque

Par souci de simplicité, ce code suppose que chaque entité a une propriété de clé primaire appelée Id. Cela peut être codifié dans une classe de base abstraite ou une interface. Vous pouvez également obtenir la propriété ou les propriétés de clé primaire à partir des métadonnées IEntityType afin que ce code fonctionne avec n’importe quel type d’entité.

Échec de définition des valeurs de clé

Les types d’entités sont souvent configurés pour utiliser valeurs de clé générées automatiquement. Il s’agit de la valeur par défaut pour les propriétés entières et GUID des clés non composites. Toutefois, si le type d’entité n’est pas configuré pour utiliser des valeurs de clé générées automatiquement, une valeur de clé explicite doit être définie avant de suivre l’entité. Par exemple, à l’aide du type d’entité suivant :

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

    public string Name { get; set; }
}

Prenons l’exemple d’un code qui tente de suivre deux nouvelles instances d’entité sans définir de valeurs pour les clés :

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

Ce code lève :

System.InvalidOperationException : l’instance du type d’entité « Pet » ne peut pas être suivie, car une autre instance avec la valeur de clé « {Id : 0} » est déjà suivie. Lors de l’attachement d’entités existantes, vérifiez qu’une seule instance d’entité avec une valeur de clé donnée est attachée.

Le correctif de ce problème consiste à définir explicitement des valeurs de clé ou à configurer la propriété de clé pour utiliser des valeurs de clé générées. Pour plus d’informations, consultez valeurs générées .

Sur-utilisant une seule instance DbContext

DbContext est conçu pour représenter une unité de travail de courte durée, comme décrit dans initialisation et configuration DbContext, et élaboré dans Suivi des modifications dans EF Core. Si vous ne suivez pas ces instructions, il est facile de s’exécuter dans des situations où une tentative est effectuée pour suivre plusieurs instances de la même entité. Voici des exemples courants :

  • L’utilisation de la même instance DbContext pour configurer l’état de test, puis exécuter le test. Cela entraîne souvent le suivi d’une instance d’entité à partir de la configuration du test, tout en essayant d’attacher une nouvelle instance dans le test approprié. Utilisez plutôt une autre instance DbContext pour configurer l’état de test et le code de test approprié.
  • Utilisation d’une instance DbContext partagée dans un référentiel ou un code similaire. Au lieu de cela, assurez-vous que votre référentiel utilise une seule instance DbContext pour chaque unité de travail.

Résolution et requêtes d’identité

La résolution d’identité se produit automatiquement lorsque les entités sont suivies à partir d’une requête. Cela signifie que si une instance d’entité avec une valeur de clé donnée est déjà suivie, cette instance suivie existante est utilisée au lieu de créer une nouvelle instance. Cela a une conséquence importante : si les données ont changé dans la base de données, cela ne sera pas reflété dans les résultats de la requête. Il s’agit d’une bonne raison d’utiliser une nouvelle instance DbContext pour chaque unité de travail, comme décrit dans initialisation et configuration DbContext, et élaboré dans Suivi des modifications dans EF Core.

Important

Il est important de comprendre qu’EF Core exécute toujours une requête LINQ sur un DbSet sur la base de données et retourne uniquement les résultats en fonction de ce qui se trouve dans la base de données. Toutefois, pour une requête de suivi, si les entités retournées sont déjà suivies, les instances suivies sont utilisées au lieu de créer des instances à partir des données de la base de données.

Reload() ou GetDatabaseValues() peut être utilisé lorsque les entités suivies doivent être actualisées avec les données les plus récentes de la base de données. Pour plus d’informations, consultez Accès aux entités suivies.

Contrairement au suivi des requêtes, les requêtes sans suivi n’effectuent pas de résolution d’identité. Cela signifie que les requêtes sans suivi peuvent retourner des doublons comme dans le cas de sérialisation JSON décrit précédemment. Cela n’est généralement pas un problème si les résultats de la requête vont être sérialisés et envoyés au client.

Conseil

N’effectuez pas régulièrement une requête sans suivi, puis attachez les entités retournées au même contexte. Cela sera plus lent et plus difficile à obtenir correctement que d’utiliser une requête de suivi.

Les requêtes sans suivi n’effectuent pas de résolution d’identité, car cela a un impact sur les performances de la diffusion en continu d’un grand nombre d’entités à partir d’une requête. Cela est dû au fait que la résolution d’identité nécessite de suivre chaque instance retournée afin qu’elle puisse être utilisée au lieu de créer ultérieurement un doublon.

Les requêtes sans suivi peuvent être forcées d’effectuer une résolution d’identité à l’aide de AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>). La requête effectue ensuite le suivi des instances retournées (sans les suivre de manière normale) et vérifie qu’aucun doublon n’est créé dans les résultats de la requête.

Substitution de l’égalité des objets

EF Core utilise égalité de référence lors de la comparaison des instances d’entité. C’est le cas même si les types d’entités remplacent Object.Equals(Object) ou modifient l’égalité des objets. Toutefois, il existe un endroit où la substitution d’égalité peut avoir un impact sur le comportement EF Core : lorsque les navigations de collection utilisent l’égalité substituée au lieu de l’égalité de référence, et signalent donc plusieurs instances comme les mêmes.

Pour cette raison, il est recommandé d’éviter la substitution de l’égalité des entités. S’il est utilisé, veillez à créer des navigations de collection qui forcent l’égalité des références. Par exemple, créez un comparateur d’égalité qui utilise l’égalité de référence :

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

(À compter de .NET 5, il est inclus dans la liste de contrôle en tant que ReferenceEqualityComparer.)

Ce comparateur peut ensuite être utilisé lors de la création de navigations de collection. Par exemple :

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

Comparaison des propriétés clés

En plus des comparaisons d’égalité, les valeurs clés doivent également être ordonnées. Cela est important pour éviter les blocages lors de la mise à jour de plusieurs entités dans un seul appel à SaveChanges. Tous les types utilisés pour les propriétés de clé primaire, alternative ou étrangère, ainsi que ceux utilisés pour les index uniques, doivent implémenter IComparable<T> et IEquatable<T>. Les types normalement utilisés en tant que clés (int, Guid, chaîne, etc.) prennent déjà en charge ces interfaces. Les types de clés personnalisés peuvent ajouter ces interfaces.