Modification des clés étrangères et des navigations

Vue d’ensemble des clés étrangères et des navigations

Les relations dans un modèle Entity Framework Core (EF Core) sont représentées à l’aide de clés étrangères (FK). Un FK se compose d’une ou plusieurs propriétés sur l’entité dépendante ou enfant dans la relation. Cette entité dépendante/enfant est associée à une entité principale/parente donnée lorsque les valeurs des propriétés de clé étrangère sur le dépendant/enfant correspondent aux valeurs des propriétés de clé principale ou primaire (PK) sur le principal/parent.

Les clés étrangères constituent un bon moyen de stocker et de manipuler des relations dans la base de données, mais elles ne sont pas très conviviales lors de l’utilisation de plusieurs entités associées dans le code d’application. Par conséquent, la plupart des modèles EF Core couchent également des « navigations » sur la représentation FK. Les navigations forment des références C#/.NET entre les instances d’entité qui reflètent les associations trouvées en correspondant aux valeurs de clé étrangère à des valeurs de clé primaire ou alternative.

Les navigations peuvent être utilisées des deux côtés de la relation, d’un côté uniquement, ou pas du tout, en laissant uniquement la propriété FK. La propriété FK peut être masquée en la rendant propriété d’ombre. Consultez relations pour plus d’informations sur la modélisation des relations.

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.

Exemple de modèle

Le modèle suivant contient quatre types d’entités avec des relations entre eux. Les commentaires du code indiquent quelles propriétés sont des clés étrangères, des clés primaires et des navigations.

public class Blog
{
    public int Id { get; set; } // Primary key
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

Les trois relations de ce modèle sont les suivantes :

  • Chaque blog peut avoir plusieurs billets (un-à-plusieurs) :
    • Blog est le principal/parent.
    • Post est le dépendant/enfant. Il contient la propriété FK Post.BlogId, dont la valeur doit correspondre à la valeur Blog.Id PK du blog associé.
    • Post.Blog est une navigation de référence d’un billet vers le blog associé. Post.Blog est la navigation inverse pour Blog.Posts.
    • Blog.Posts est une navigation de collection d’un blog vers tous les billets associés. Blog.Posts est la navigation inverse pour Post.Blog.
  • Chaque blog peut avoir une seule ressources (un-à-un) :
    • Blog est le principal/parent.
    • BlogAssets est le dépendant/enfant. Il contient la propriété FK BlogAssets.BlogId, dont la valeur doit correspondre à la valeur Blog.Id PK du blog associé.
    • BlogAssets.Blog est une navigation de référence des ressources vers le blog associé. BlogAssets.Blog est la navigation inverse pour Blog.Assets.
    • Blog.Assets est une navigation de référence du blog vers les ressources associées. Blog.Assets est la navigation inverse pour BlogAssets.Blog.
  • Chaque publication peut avoir de nombreuses balises et chaque balise peut avoir de nombreux billets (plusieurs-à-plusieurs) :
    • Les relations plusieurs-à-plusieurs sont une couche supplémentaire sur deux relations un-à-plusieurs. Les relations plusieurs-à-plusieurs sont abordées plus loin dans ce document.
    • Post.Tags est une navigation de collection d’un billet vers toutes les balises associées. Post.Tags est la navigation inverse pour Tag.Posts.
    • Tag.Posts est une navigation de collection d’une balise vers toutes les publications associées. Tag.Posts est la navigation inverse pour Post.Tags.

Consultez relations pour plus d’informations sur la façon de modéliser et de configurer des relations.

Correctif de relation

EF Core maintient les navigations en alignement avec les valeurs de clé étrangère et inversement. Autrement dit, si une valeur de clé étrangère change de sorte qu’elle fait maintenant référence à une entité principale/parent différente, les navigations sont mises à jour pour refléter cette modification. De même, si une navigation est modifiée, les valeurs de clé étrangère des entités impliquées sont mises à jour pour refléter cette modification. C’est ce qu’on appelle « correctif de relation ».

Correction par requête

La correction se produit d’abord lorsque les entités sont interrogées à partir de la base de données. La base de données a uniquement des valeurs de clé étrangère. Par conséquent, lorsque EF Core crée une instance d’entité à partir de la base de données, elle utilise les valeurs de clé étrangère pour définir les navigations de référence et ajouter des entités aux navigations de collection selon les besoins. Par exemple, considérez une requête pour les blogs et ses publications et ressources associées :

using var context = new BlogsContext();

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

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Pour chaque blog, EF Core crée d’abord une instance de Blog. Ensuite, à mesure que chaque billet est chargé à partir de la base de données, sa navigation de référence Post.Blog est définie pour pointer vers le blog associé. De même, le billet est ajouté à la navigation de collection Blog.Posts. La même chose se produit avec BlogAssets, sauf dans ce cas les deux navigations sont des références. La navigation Blog.Assets est définie pour pointer vers l’instance de ressources, et la navigation BlogAsserts.Blog est définie sur l’instance de blog.

En examinant lavue de débogage de suivi des modifications après cette requête montre deux blogs, chacun avec une seule ressources et deux publications en cours de suivi :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

La vue débogage affiche à la fois les valeurs de clé et les navigations. Les navigations sont affichées à l’aide des valeurs de clé primaire des entités associées. Par exemple, Posts: [{Id: 1}, {Id: 2}] dans la sortie ci-dessus indique que la navigation Blog.Posts collection contient deux publications connexes avec les clés primaires 1 et 2 respectivement. De même, pour chaque billet associé au premier blog, la ligne Blog: {Id: 1} indique que la navigation Post.Blog fait référence au blog avec la clé primaire 1.

Correction des entités suivies localement

La correction de relation se produit également entre les entités retournées à partir d’une requête de suivi et d’entités déjà suivies par DbContext. Par exemple, envisagez d’exécuter trois requêtes distinctes pour les blogs, les billets et les ressources :

using var context = new BlogsContext();

var blogs = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = context.Assets.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = context.Posts.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

En examinant à nouveau les vues de débogage, après la première requête, seuls les deux blogs sont suivis :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Les navigations de référence Blog.Assets sont null et les navigations de collection Blog.Posts sont vides, car aucune entité associée n’est actuellement suivie par le contexte.

Après la deuxième requête, les navigations de référence Blogs.Assets ont été corrigées jusqu’à ce qu’elles pointent vers les instances nouvellement suivies BlogAsset. De même, les navigations de référence BlogAssets.Blog sont définies pour pointer vers l’instance Blog déjà suivie appropriée.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

Enfin, après la troisième requête, les navigations de collection Blog.Posts contiennent désormais toutes les publications associées, et les références Post.Blog pointent vers l’instance de Blog appropriée :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Il s’agit du même état final que celui obtenu avec la requête unique d’origine, car EF Core a corrigé les navigations que les entités ont été suivies, même en provenance de plusieurs requêtes différentes.

Remarque

La correction n’entraîne jamais le retour de données supplémentaires à partir de la base de données. Il connecte uniquement les entités déjà retournées par la requête ou déjà suivies par DbContext. Consultez résolution d’identité dans EF Core pour plus d’informations sur la gestion des doublons lors de la sérialisation des entités.

Modification des relations à l’aide de navigations

Le moyen le plus simple de modifier la relation entre deux entités consiste à manipuler une navigation, tout en laissant EF Core corriger les valeurs de navigation inverse et FK de manière appropriée. Pour ce faire, vous pouvez :

  • Ajout ou suppression d’une entité d’une navigation de collection.
  • Modification d’une navigation de référence pour pointer vers une autre entité ou la définir sur Null.

Ajout ou suppression de navigations de collection

Par exemple, nous allons déplacer l’un des billets du blog Visual Studio vers le blog .NET. Cela nécessite d’abord charger les blogs et les billets, puis déplacer le billet de la collection de navigation sur un blog vers la collection de navigation sur l’autre blog :

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == "Visual Studio Blog");

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Conseil

Un appel à ChangeTracker.DetectChanges() est nécessaire ici, car l’accès à la vue de débogage n’entraîne pas détection automatique des modifications.

Il s’agit de l’affichage de débogage imprimé après avoir exécuté le code ci-dessus :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

La navigation Blog.Posts sur le blog .NET comporte désormais trois billets (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). De même, la navigation Blog.Posts sur le blog Visual Studio n’a qu’un seul billet (Posts: [{Id: 4}]). Cela doit être attendu, car le code a explicitement modifié ces collections.

Plus intéressant encore, même si le code n’a pas explicitement modifié la navigation Post.Blog , il a été résolu pour pointer vers le blog Visual Studio (Blog: {Id: 1}). En outre, la Post.BlogId valeur de clé étrangère a été mise à jour pour correspondre à la valeur de clé primaire du blog .NET. Cette modification de la valeur FK est ensuite conservée dans la base de données lorsque SaveChanges est appelé :

-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

Modification des navigations de référence

Dans l’exemple précédent, un billet a été déplacé d’un blog à un autre en manipulant la navigation de collection de billets sur chaque blog. La même chose peut être obtenue en modifiant plutôt la navigation de référence Post.Blog pour pointer vers le nouveau blog. Par exemple :

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

La vue de débogage après cette modification est exactement la même que dans l’exemple précédent. Cela est dû au fait qu’EF Core a détecté le changement de navigation de référence, puis corrigé les navigations de collection et la valeur FK à mettre en correspondance.

Modification des relations à l’aide de valeurs de clé étrangère

Dans la section précédente, les relations ont été manipulées par des navigations laissant les valeurs de clé étrangère être mises à jour automatiquement. Il s’agit de la méthode recommandée pour manipuler les relations dans EF Core. Toutefois, il est également possible de manipuler directement les valeurs FK. Par exemple, nous pouvons déplacer un billet d’un blog vers un autre en modifiant la valeur de clé étrangère Post.BlogId :

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

Notez comment cela est très similaire à la modification de la navigation de référence, comme illustré dans l’exemple précédent.

La vue de débogage après cette modification est à nouveau exactement la même que le cas pour les deux exemples précédents. Cela est dû au fait que EF Core a détecté la modification de la valeur FK, puis corrigé les navigations de référence et de collection à mettre en correspondance.

Conseil

N’écrivez pas de code pour manipuler toutes les navigations et valeurs FK chaque fois qu’une relation change. Ce code est plus compliqué et doit garantir des modifications cohérentes des clés étrangères et des navigations dans tous les cas. Si possible, il suffit de manipuler une seule navigation, ou peut-être les deux navigations. Si nécessaire, il vous suffit de manipuler les valeurs FK. Évitez de manipuler à la fois les navigations et les valeurs FK.

Correctif pour les entités ajoutées ou supprimées

Ajout à une navigation de collection

EF Core effectue les actions suivantes lorsqu’il détecte qu’une nouvelle entité dépendante/enfant a été ajoutée à une navigation de collection :

  • Si l’entité n’est pas suivie, elle est suivie. (L’entité est généralement dans l’état Added . Toutefois, si le type d’entité est configuré pour utiliser des clés générées et que la valeur de clé primaire est définie, l’entité est suivie dans l’état Unchanged .)
  • Si l’entité est associée à un autre principal/parent, cette relation est rompue.
  • L’entité devient associée au principal/parent propriétaire de la navigation de collection.
  • Les navigations et les valeurs de clé étrangère sont corrigées pour toutes les entités impliquées.

En fonction de cela, nous pouvons voir que pour déplacer un billet d’un blog vers un autre, nous n’avons pas réellement besoin de le supprimer de l’ancienne navigation de collection avant de l’ajouter au nouveau. Par conséquent, le code de l’exemple ci-dessus peut être modifié à partir de :

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

Par :

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

EF Core voit que le billet a été ajouté à un nouveau blog et le supprime automatiquement de la collection sur le premier blog.

Suppression d’une navigation de collection

La suppression d’une entité dépendante/enfant de la navigation de collection du principal/parent entraîne la séparation de la relation avec ce principal/parent. Ce qui se passe ensuite dépend si la relation est facultative ou obligatoire.

Relations facultatives

Par défaut, pour les relations facultatives, la valeur de clé étrangère est définie sur Null. Cela signifie que le dépendant/enfant n’est plus associé à tout principal/parent. Par exemple, nous allons charger un blog et des billets, puis supprimer l’un des billets de la navigation de collection Blog.Posts :

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

En examinant la vue de débogage de suivi des modifications après cette modification montre que :

  • Le FK Post.BlogId a été défini sur Null (BlogId: <null> FK Modified Originally 1)
  • La navigation de référence Post.Blog a été définie sur Null (Blog: <null>)
  • Le billet a été supprimé de la navigation dans Blog.Posts collection (Posts: [{Id: 1}])
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Notez que le billet n’est pas marqué comme Deleted. Elle est marquée comme Modified afin que la valeur FK de la base de données soit définie sur null lorsque SaveChanges est appelé.

Relations requises

La définition de la valeur FK sur Null n’est pas autorisée (et n’est généralement pas possible) pour les relations requises. Par conséquent, la séparation d’une relation requise signifie que l’entité dépendante/enfant doit être re-parentée vers un nouveau principal/parent, ou supprimée de la base de données lorsque SaveChanges est appelé pour éviter une violation de contrainte référentielle. Il s’agit de « suppression d’orphelins » et est le comportement par défaut dans EF Core pour les relations requises.

Par exemple, nous allons modifier la relation entre le blog et les billets pour qu’elles soient requises, puis exécuter le même code que dans l’exemple précédent :

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

Après cette modification, l’affichage de débogage montre que :

  • Le billet a été marqué comme Deleted de sorte qu’il soit supprimé de la base de données lorsque SaveChanges est appelé.
  • La navigation de référence Post.Blog a été définie sur Null (Blog: <null>).
  • Le billet a été supprimé de la navigation dans Blog.Posts collection (Posts: [{Id: 1}]).
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Notez que le Post.BlogId reste inchangé, car pour une relation requise, il ne peut pas être défini sur Null.

L’appel de SaveChanges entraîne la suppression du billet orphelin :

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Supprimer le minutage et le re-parentage des orphelins

Par défaut, le marquage des orphelins comme Deleted se produit dès que le changement de relation est détecté. Toutefois, ce processus peut être retardé jusqu’à ce que SaveChanges soit réellement appelé. Cela peut être utile pour éviter de rendre les orphelins d’entités qui ont été supprimées d’un principal/parent, mais seront re-parentées avec un nouveau principal/parent avant l’appel de SaveChanges. ChangeTracker.DeleteOrphansTiming est utilisé pour définir ce minutage. Par exemple :

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Après avoir supprimé le billet de la première collection, l’objet n’est pas marqué comme Deleted tel qu’il était dans l’exemple précédent. Au lieu de cela, EF Core suit que la relation est rompue même s’il s’agit d’une relation requise. (La valeur FK est considérée comme null par EF Core, même si elle ne peut pas vraiment être null, car le type n’est pas nullable. Il s’agit d’un « null conceptuel ».)

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

L’appel de SaveChanges à ce stade entraînerait la suppression du billet orphelin. Toutefois, si comme dans l’exemple ci-dessus, le billet est associé à un nouveau blog avant que SaveChanges soit appelé, il sera corrigé de manière appropriée à ce nouveau blog et n’est plus considéré comme orphelin :

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

SaveChanges appelé à ce stade met à jour le billet dans la base de données plutôt que de le supprimer.

Il est également possible de désactiver la suppression automatique des orphelins. Cela entraîne une exception si SaveChanges est appelé alors qu’un orphelin est suivi. Par exemple, ce code :

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

context.SaveChanges(); // Throws

Lève cette exception :

System.InvalidOperationException : L’association entre les entités « Blog » et « Post » avec la valeur de clé « {BlogId : 1} » a été interrompue, mais la relation est marquée comme obligatoire ou est implicitement requise, car la clé étrangère n’est pas nullable. Si l’entité dépendante/enfant doit être supprimée lorsqu’une relation requise est rompue, configurez la relation pour utiliser des suppressions en cascade.

La suppression des orphelins, ainsi que les suppressions en cascade, peut être forcée à tout moment en appelant ChangeTracker.CascadeChanges(). La combinaison de ce paramètre avec la définition du minutage orphelin de suppression sur Never garantit que les orphelins ne sont jamais supprimés, sauf si EF Core est explicitement invité à le faire.

Modification d’une navigation de référence

La modification de la navigation de référence d’une relation un-à-plusieurs a le même effet que la modification de la navigation de collection à l’autre extrémité de la relation. La définition de la navigation de référence de dépendant/enfant sur null équivaut à supprimer l’entité de la navigation de collection du principal/parent. Toutes les modifications de correctif et de base de données se produisent comme décrit dans la section précédente, notamment en rendant l’entité orpheline si la relation est requise.

Relations facultatives un-à-un

Pour les relations un-à-un, la modification d’une navigation de référence entraîne une rupture de toute relation précédente. Pour les relations facultatives, cela signifie que la valeur FK sur la dépendance/enfant précédemment associée est définie sur Null. Par exemple :

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

La vue de débogage avant d’appeler SaveChanges indique que les nouvelles ressources ont remplacé les ressources existantes, qui sont désormais marquées comme Modified avec une valeur de clé de chiffrement null BlogAssets.BlogId :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

Cela entraîne une mise à jour et une insertion lorsque SaveChanges est appelé :

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Relations un-à-un requises

L’exécution du même code que dans l’exemple précédent, mais cette fois avec une relation un-à-un requise, montre que le BlogAssets précédemment associé est désormais marqué comme Deleted, car il devient orphelin lorsque la nouvelle BlogAssets a sa place :

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

Cela entraîne ensuite une suppression et une insertion lorsque SaveChanges est appelé :

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Le minutage des orphelins marqués comme supprimés peut être modifié de la même façon que celui indiqué pour les navigations de collection et a les mêmes effets.

Suppression d'une entité

Relations facultatives

Lorsqu’une entité est marquée comme Deleted, par exemple en appelant DbContext.Remove, les références à l’entité supprimée sont supprimées des navigations d’autres entités. Pour les relations facultatives, les valeurs FK dans les entités dépendantes sont définies sur Null.

Par exemple, nous allons marquer le blog Visual Studio comme Deleted:

using var context = new BlogsContext();

var vsBlog = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

En examinant lavue de débogage de suivi des modifications avant d’appeler SaveChanges affiche :

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

Notez que :

  • Le blog est marqué comme Deleted.
  • Les ressources associées au blog supprimé ont une valeur FK null (BlogId: <null> FK Modified Originally 2) et une navigation de référence Null (Blog: <null>)
  • Chaque billet lié au blog supprimé a une valeur FK null (BlogId: <null> FK Modified Originally 2) et une navigation de référence Null (Blog: <null>)

Relations requises

Le comportement de correction des relations requises est identique à celui des relations facultatives, sauf que les entités dépendantes/enfants sont marquées comme Deleted , car elles ne peuvent pas exister sans principal/parent et doivent être supprimées de la base de données lorsque SaveChanges est appelé pour éviter une exception de contrainte référentielle. Il s’agit de « suppression en cascade » et est le comportement par défaut dans EF Core pour les relations requises. Par exemple, l’exécution du même code que dans l’exemple précédent, mais avec une relation requise entraîne l’affichage de débogage suivant avant que SaveChanges soit appelé :

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Comme prévu, les personnes dépendantes/enfants sont désormais marquées comme Deleted. Toutefois, notez que les navigations sur les entités supprimées n’ont pas modifiées. Cela peut sembler étrange, mais il évite de déchiqueter complètement un graphique d’entités supprimé en désactivant toutes les navigations. Autrement dit, le blog, la ressource et les billets forment toujours un graphique d’entités même après avoir été supprimés. Cela facilite grandement la suppression d’un graphique d’entités que dans EF6 où le graphique a été déchiqueté.

Minutage de suppression en cascade et re-parentage

Par défaut, la suppression en cascade se produit dès que le parent/principal est marqué comme Deleted. Il s’agit de la même chose que pour la suppression d’orphelins, comme décrit précédemment. Comme pour la suppression des orphelins, ce processus peut être retardé jusqu’à ce que SaveChanges soit appelé, ou même désactivé entièrement, en définissant ChangeTracker.CascadeDeleteTiming de manière appropriée. Cela est utile de la même façon que pour supprimer des orphelins, y compris pour les enfants/personnes dépendantes après la suppression d’un principal/parent.

Les suppressions en cascade, ainsi que la suppression d’orphelins, peuvent être forcées à tout moment en appelant ChangeTracker.CascadeChanges(). La combinaison de ce paramètre avec la définition du minutage de suppression en cascade sur Never garantit que les suppressions en cascade ne se produisent jamais, sauf si EF Core est explicitement invité à le faire.

Conseil

La suppression en cascade et la suppression d’orphelins sont étroitement liées. Les deux entraînent la suppression d’entités dépendantes/enfants lorsque la relation avec le principal/parent requis est interrompue. Pour la suppression en cascade, ce problème se produit car le principal/le parent est lui-même supprimé. Pour les orphelins, l’entité principale/parente existe toujours, mais n’est plus liée aux entités dépendantes/enfants.

Relations plusieurs-à-plusieurs

Les relations plusieurs-à-plusieurs dans EF Core sont implémentées à l’aide d’une entité de jointure. Chaque côté de la relation plusieurs-à-plusieurs est lié à cette entité de jointure avec une relation un-à-plusieurs. Cette entité de jointure peut être explicitement définie et mappée, ou elle peut être créée implicitement et masquée. Dans les deux cas, le comportement sous-jacent est le même. Nous allons d’abord examiner ce comportement sous-jacent pour comprendre comment le suivi des relations plusieurs-à-plusieurs fonctionne.

Fonctionnement des relations plusieurs-à-plusieurs

Considérez ce modèle EF Core qui crée une relation plusieurs-à-plusieurs entre les publications et les balises à l’aide d’un type d’entité de jointure explicitement défini :

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Notez que le type d’entité de jointure PostTag contient deux propriétés de clé étrangère. Dans ce modèle, pour qu’un billet soit lié à une balise, il doit y avoir une entité de jointure PostTag où le PostTag.PostId valeur de clé étrangère correspond à la valeur de clé primaire Post.Id et où la valeur de clé étrangère PostTag.TagId correspond à la valeur de clé primaire Tag.Id . Par exemple :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

En examinant la vue de débogage de suivi des modifications après l’exécution de ce code, le billet et la balise sont liés par la nouvelle entité de jointure PostTag :

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

Notez que les navigations de collection sur Post et Tag ont été corrigées, comme les navigations de référence sur PostTag. Ces relations peuvent être manipulées par des navigations au lieu de valeurs FK, comme dans tous les exemples précédents. Par exemple, le code ci-dessus peut être modifié pour ajouter la relation en définissant les navigations de référence sur l’entité de jointure :

context.Add(new PostTag { Post = post, Tag = tag });

Cela entraîne exactement la même modification des clés et des navigations que dans l’exemple précédent.

Ignorer les navigations

La manipulation manuelle de la table de jointure peut être fastidieuse. Les relations plusieurs-à-plusieurs peuvent être manipulées directement à l’aide de navigations de collection spéciales qui « ignorent » l’entité de jointure. Par exemple, deux navigations skip peuvent être ajoutées au modèle ci-dessus ; d’une publication à des balises, et l’autre d’une balise à des publications :

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Cette relation plusieurs-à-plusieurs nécessite la configuration suivante pour vous assurer que les navigations skip et les navigations normales sont toutes utilisées pour la même relation plusieurs-à-plusieurs :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

Consultez relations pour plus d’informations sur le mappage de relations plusieurs-à-plusieurs.

Ignorez l’apparence et le comportement des navigations de collection normales. Toutefois, la façon dont elles fonctionnent avec des valeurs de clé étrangère est différente. Nous allons associer un billet à une balise, mais cette fois à l’aide d’un saut de navigation :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Notez que ce code n’utilise pas l’entité de jointure. Il ajoute plutôt une entité à une collection de navigation de la même façon que si cela était une relation un-à-plusieurs. La vue de débogage résultante est essentiellement la même qu’avant :

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

Notez qu’une instance de l’entité de jointure PostTag a été créée automatiquement avec les valeurs FK définies sur les valeurs PK de la balise et du billet qui sont désormais associées. Toutes les navigations de référence et de collection normales ont été corrigées pour correspondre à ces valeurs FK. De plus, étant donné que ce modèle contient des navigations ignorées, celles-ci ont également été corrigées. Plus précisément, même si nous avons ajouté la balise au Post.Tags ignorer la navigation, la Tag.Posts navigation inverse ignorer l’autre côté de cette relation a également été corrigée pour contenir le billet associé.

Il est important de noter que les relations plusieurs-à-plusieurs sous-jacentes peuvent toujours être manipulées directement même lorsque les navigations ignorées ont été superposées. Par exemple, la balise et la publication peuvent être associées comme nous l’avons fait avant d’introduire des navigations ignorer :

context.Add(new PostTag { Post = post, Tag = tag });

Ou à l’aide de valeurs FK :

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Cela entraîne toujours la correction correcte des navigations skip, ce qui entraîne la même sortie de vue de débogage que dans l’exemple précédent.

Ignorer les navigations uniquement

Dans la section précédente, nous avons ajouté ignorer les navigations en plus de définir entièrement les deux relations un-à-plusieurs sous-jacentes. Cela est utile pour illustrer ce qui arrive aux valeurs FK, mais est souvent inutile. Au lieu de cela, la relation plusieurs-à-plusieurs peut être définie à l’aide de ignorer uniquement les navigations. Il s’agit de la façon dont la relation plusieurs-à-plusieurs est définie dans le modèle en haut de ce document. À l’aide de ce modèle, nous pouvons à nouveau associer un billet et une balise en ajoutant un billet au Tag.Posts ignorer la navigation (ou, alternativement, en ajoutant une balise au Post.Tags ignorer la navigation) :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Après avoir apporté cette modification à la vue de débogage, EF Core a créé une instance de Dictionary<string, object> pour représenter l’entité de jointure. Cette entité de jointure contient à la fois PostsId et TagsId propriétés de clé étrangère qui ont été définies pour correspondre aux valeurs PK de la publication et de la balise associées.

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

Consultez relations pour plus d’informations sur les entités de jointure implicite et l’utilisation de types d’entités Dictionary<string, object>.

Important

Le type CLR utilisé pour les types d’entités de jointure par convention peut changer dans les futures versions afin d’améliorer les performances. Ne dépendez pas du type de jointure Dictionary<string, object>, sauf s’il a été configuré explicitement.

Joindre des entités avec des charges utiles

Jusqu’à présent, tous les exemples ont utilisé un type d’entité de jointure (explicite ou implicite) qui contient uniquement les deux propriétés de clé étrangère nécessaires pour la relation plusieurs-à-plusieurs. Aucune de ces valeurs FK ne doit être explicitement définie par l’application lors de la manipulation des relations, car leurs valeurs proviennent des propriétés de clé primaire des entités associées. Cela permet à EF Core de créer des instances de l’entité de jointure sans données manquantes.

Charges utiles avec des valeurs générées

EF Core prend en charge l’ajout de propriétés supplémentaires au type d’entité de jointure. Il s’agit de donner à l’entité de jointure une « charge utile ». Par exemple, ajoutons TaggedOn propriété à l’entité de jointure PostTag :

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

Cette propriété de charge utile ne sera pas définie lorsque EF Core crée une instance d’entité de jointure. La façon la plus courante de traiter cela consiste à utiliser des propriétés de charge utile avec des valeurs générées automatiquement. Par exemple, la propriété TaggedOn peut être configurée pour utiliser un horodatage généré par le magasin lorsque chaque nouvelle entité est insérée :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

Un billet peut maintenant être étiqueté de la même façon que précédemment :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

En examinant lavue de débogage du suivi des modifications après l’appel de SaveChanges, la propriété de charge utile a été définie de manière appropriée :

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

Définition explicite des valeurs de charge utile

À partir de l’exemple précédent, nous allons ajouter une propriété de charge utile qui n’utilise pas de valeur générée automatiquement :

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

Un billet peut maintenant être étiqueté de la même façon que précédemment, et l’entité de jointure sera toujours créée automatiquement. Cette entité est alors accessible à l’aide de l’un des mécanismes décrits dans Accès aux entités suivies. Par exemple, le code ci-dessous utilise DbSet<TEntity>.Find pour accéder à l’instance d’entité de jointure :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = context.Set<PostTag>().Find(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Une fois que l’entité de jointure a été localisée, elle peut être manipulée de manière normale dans cet exemple pour définir la propriété de charge utile TaggedBy avant d’appeler SaveChanges.

Remarque

Notez qu’un appel à ChangeTracker.DetectChanges() est requis ici pour permettre à EF Core de détecter le changement de propriété de navigation et de créer l’instance d’entité de jointure avant d’utiliser Find. Pour plus d’informations, consultez détection des modifications et notifications.

Vous pouvez également créer explicitement l’entité de jointure pour associer un billet à une balise. Par exemple :

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Enfin, une autre façon de définir des données de charge utile consiste à remplacer SaveChanges ou à utiliser l’événement DbContext.SavingChanges pour traiter les entités avant de mettre à jour la base de données. Par exemple :

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}