Modifica di chiavi esterne e spostamenti

Panoramica delle chiavi esterne e degli spostamenti

Le relazioni in un modello Entity Framework Core (EF Core) vengono rappresentate usando chiavi esterne (FK). Un FK è costituito da una o più proprietà dell'entità dipendente o figlio nella relazione. Questa entità dipendente/figlio è associata a una determinata entità principale/padre quando i valori delle proprietà della chiave esterna sul dipendente/figlio corrispondono ai valori delle proprietà chiave alternativa o primaria (PK) nell'entità principale/padre.

Le chiavi esterne sono un buon modo per archiviare e modificare le relazioni nel database, ma non sono molto semplici quando si usano più entità correlate nel codice dell'applicazione. Pertanto, la maggior parte dei modelli EF Core esegue anche il layer di navigazione sulla rappresentazione FK. Gli spostamenti formano riferimenti C#/.NET tra istanze di entità che riflettono le associazioni trovate mediante la corrispondenza dei valori di chiave esterna ai valori di chiave primaria o alternativa.

Gli spostamenti possono essere usati su entrambi i lati della relazione, solo su un lato o non affatto, lasciando solo la proprietà FK. La proprietà FK può essere nascosta rendendola una proprietà shadow. Per altre informazioni sulla modellazione delle relazioni, vedere Relazioni .

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.

Modello di esempio

Il modello seguente contiene quattro tipi di entità con relazioni tra di esse. I commenti nel codice indicano quali proprietà sono chiavi esterne, chiavi primarie e spostamenti.

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
}

Le tre relazioni in questo modello sono:

  • Ogni blog può avere molti post (uno-a-molti):
    • Blog è l'entità/padre.
    • Post è il dipendente/figlio. Contiene la proprietà Post.BlogIdFK , il cui valore deve corrispondere al Blog.Id valore PK del blog correlato.
    • Post.Blog è una navigazione di riferimento da un post al blog associato. Post.Blog è lo spostamento inverso per Blog.Posts.
    • Blog.Posts è una navigazione raccolta da un blog a tutti i post associati. Blog.Posts è lo spostamento inverso per Post.Blog.
  • Ogni blog può avere uno asset (uno-a-uno):
    • Blog è l'entità/padre.
    • BlogAssets è il dipendente/figlio. Contiene la proprietà BlogAssets.BlogIdFK , il cui valore deve corrispondere al Blog.Id valore PK del blog correlato.
    • BlogAssets.Blog è uno spostamento di riferimento dagli asset al blog associato. BlogAssets.Blog è lo spostamento inverso per Blog.Assets.
    • Blog.Assets è uno spostamento di riferimento dal blog agli asset associati. Blog.Assets è lo spostamento inverso per BlogAssets.Blog.
  • Ogni post può avere molti tag e ogni tag può avere molti post (molti-a-molti):
    • Le relazioni molti-a-molti sono un ulteriore livello su due relazioni uno-a-molti. Le relazioni molti-a-molti sono descritte più avanti in questo documento.
    • Post.Tags è uno spostamento di raccolta da un post a tutti i tag associati. Post.Tags è lo spostamento inverso per Tag.Posts.
    • Tag.Posts è una navigazione di raccolta da un tag a tutti i post associati. Tag.Posts è lo spostamento inverso per Post.Tags.

Per altre informazioni su come modellare e configurare le relazioni, vedere Relazioni .

Correzione delle relazioni

EF Core mantiene gli spostamenti in allineamento con i valori di chiave esterna e viceversa. Ovvero, se un valore di chiave esterna cambia in modo che ora faccia riferimento a un'entità principale/padre diversa, le navigazione vengono aggiornate in modo da riflettere questa modifica. Analogamente, se viene modificata una struttura di spostamento, i valori di chiave esterna delle entità coinvolte vengono aggiornati in modo da riflettere questa modifica. Questa operazione è denominata "correzione delle relazioni".

Correzione per query

La correzione si verifica prima quando vengono eseguite query sulle entità dal database. Il database ha solo valori di chiave esterna, quindi quando EF Core crea un'istanza di entità dal database usa i valori di chiave esterna per impostare gli spostamenti di riferimento e aggiungere entità agli spostamenti della raccolta in base alle esigenze. Si consideri, ad esempio, una query per i blog e i relativi post e asset associati:

using var context = new BlogsContext();

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

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

Per ogni blog, EF Core creerà prima di tutto un'istanza Blog . Quindi, quando ogni post viene caricato dal database, la navigazione Post.Blog di riferimento viene impostata in modo che punti al blog associato. Analogamente, il post viene aggiunto al riquadro di spostamento della Blog.Posts raccolta. La stessa cosa accade con BlogAssets, tranne in questo caso entrambi gli spostamenti sono riferimenti. La Blog.Assets struttura di spostamento è impostata in modo che punti all'istanza degli asset e la BlogAsserts.Blog navigazione sia impostata in modo che punti all'istanza del blog.

Esaminando la visualizzazione debug di Rilevamento modifiche dopo che questa query mostra due blog, ognuno con uno asset e due post monitorati:

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 visualizzazione di debug mostra sia i valori chiave che gli spostamenti. Gli spostamenti vengono visualizzati usando i valori di chiave primaria delle entità correlate. Nell'output precedente, ad esempio, Posts: [{Id: 1}, {Id: 2}] l'esplorazione della Blog.Posts raccolta contiene due post correlati rispettivamente con chiavi primarie 1 e 2. Analogamente, per ogni post associato al primo blog, la Blog: {Id: 1} riga indica che lo Post.Blog spostamento fa riferimento al blog con la chiave primaria 1.

Correzione alle entità rilevate in locale

La correzione delle relazioni si verifica anche tra le entità restituite da una query di rilevamento e le entità già rilevate da DbContext. Si consideri ad esempio l'esecuzione di tre query separate per blog, post e asset:

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

Esaminando di nuovo le viste di debug, dopo la prima query vengono rilevati solo i due blog:

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: []

Gli Blog.Assets spostamenti dei riferimenti sono Null e gli spostamenti della Blog.Posts raccolta sono vuoti perché non vengono attualmente rilevate entità associate dal contesto.

Dopo la seconda query, gli Blogs.Assets spostamenti di riferimento sono stati risolti in modo da puntare alle istanze appena rilevate BlogAsset . Analogamente, gli BlogAssets.Blog spostamenti di riferimento vengono impostati in modo che puntino all'istanza già rilevata Blog appropriata.

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}

Infine, dopo la terza query, gli spostamenti della Blog.Posts raccolta ora contengono tutti i post correlati e i Post.Blog riferimenti puntano all'istanza appropriata Blog :

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: []

Si tratta dello stesso stato finale ottenuto con la singola query originale, poiché EF Core ha corretto gli spostamenti come entità sono state rilevate, anche quando provengono da più query diverse.

Nota

La correzione non comporta mai la restituzione di più dati dal database. Connette solo le entità già restituite dalla query o già rilevate da DbContext. Per informazioni sulla gestione dei duplicati durante la serializzazione delle entità, vedere Risoluzione delle identità in EF Core .

Modifica delle relazioni tramite gli spostamenti

Il modo più semplice per modificare la relazione tra due entità consiste nel modificare uno spostamento, lasciando EF Core per correggere i valori di navigazione inversa e FK in modo appropriato. A tale scopo, è possibile:

  • Aggiunta o rimozione di un'entità da una navigazione nella raccolta.
  • Modifica di una navigazione di riferimento in modo che punti a un'entità diversa o impostarla su Null.

Aggiunta o rimozione dagli spostamenti della raccolta

Ad esempio, spostare uno dei post dal blog di Visual Studio al blog di .NET. Ciò richiede prima di tutto il caricamento dei blog e dei post e quindi lo spostamento del post dalla raccolta di navigazione in un blog alla raccolta di navigazione nell'altro 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();

Suggerimento

In questo caso è necessaria una chiamata a ChangeTracker.DetectChanges() perché l'accesso alla visualizzazione di debug non causa il rilevamento automatico delle modifiche.

Questa è la visualizzazione di debug stampata dopo l'esecuzione del codice precedente:

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 Blog.Posts navigazione nel blog .NET include ora tre post (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Analogamente, lo Blog.Posts spostamento nel blog di Visual Studio include un solo post (Posts: [{Id: 4}]). Questa operazione deve essere prevista perché il codice ha modificato in modo esplicito queste raccolte.

Più interessante, anche se il codice non ha modificato in modo esplicito lo Post.Blog spostamento, è stato corretto in modo da puntare al blog di Visual Studio (Blog: {Id: 1}). Inoltre, il Post.BlogId valore della chiave esterna è stato aggiornato in modo che corrisponda al valore della chiave primaria del blog .NET. Questa modifica al valore FK in viene quindi salvata in modo permanente nel database quando viene chiamato SaveChanges:

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

Modifica degli spostamenti dei riferimenti

Nell'esempio precedente, un post è stato spostato da un blog a un altro modificando lo spostamento della raccolta di post in ogni blog. La stessa cosa può essere ottenuta modificando invece la Post.Blog navigazione di riferimento in modo che punti al nuovo blog. Ad esempio:

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

La visualizzazione di debug dopo questa modifica è esattamente la stessa dell'esempio precedente. Questo perché EF Core ha rilevato la modifica della navigazione di riferimento e quindi corretto gli spostamenti della raccolta e il valore FK in modo che corrispondano.

Modifica delle relazioni tramite valori di chiave esterna

Nella sezione precedente le relazioni sono state modificate dagli spostamenti lasciando i valori di chiave esterna da aggiornare automaticamente. Questo è il modo consigliato per modificare le relazioni in EF Core. Tuttavia, è anche possibile modificare direttamente i valori FK. Ad esempio, è possibile spostare un post da un blog a un altro modificando il valore della Post.BlogId chiave esterna:

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

Si noti che questo comportamento è molto simile alla modifica dello spostamento di riferimento, come illustrato nell'esempio precedente.

La visualizzazione di debug dopo questa modifica è di nuovo identica a quella dei due esempi precedenti. Questo è dovuto al fatto che EF Core ha rilevato la modifica del valore FK e quindi corretto sia gli spostamenti di riferimento che di raccolta in modo che corrispondano.This is because EF Core detect the FK value change and then fixed up both the reference and collection navigations to match.

Suggerimento

Non scrivere codice per modificare tutti gli spostamenti e i valori FK ogni volta che una relazione cambia. Tale codice è più complesso e deve garantire modifiche coerenti a chiavi esterne e spostamenti in ogni caso. Se possibile, è sufficiente modificare una singola navigazione o forse entrambi gli spostamenti. Se necessario, è sufficiente modificare i valori FK. Evitare di modificare sia gli spostamenti che i valori FK.

Correzione per le entità aggiunte o eliminate

Aggiunta a una struttura di spostamento di una raccolta

EF Core esegue le azioni seguenti quando rileva che una nuova entità dipendente/figlio è stata aggiunta a una navigazione raccolta:

  • Se l'entità non viene rilevata, viene rilevata. L'entità è in genere nello Added stato . Tuttavia, se il tipo di entità è configurato per l'uso di chiavi generate e viene impostato il valore della chiave primaria, l'entità viene rilevata nello Unchanged stato.
  • Se l'entità è associata a un'entità principale/padre diversa, tale relazione viene interrotta.
  • L'entità diventa associata all'entità principale/padre proprietaria della struttura di spostamento della raccolta.
  • Gli spostamenti e i valori di chiave esterna vengono risolti per tutte le entità coinvolte.

In base a questo è possibile vedere che per spostare un post da un blog a un altro non è effettivamente necessario rimuoverlo dalla struttura di spostamento della raccolta precedente prima di aggiungerlo al nuovo. Il codice dell'esempio precedente può quindi essere modificato da:

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

Con:

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

EF Core rileva che il post è stato aggiunto a un nuovo blog e lo rimuove automaticamente dalla raccolta nel primo blog.

Rimozione da una navigazione raccolta

La rimozione di un'entità dipendente/figlio dalla navigazione nella raccolta dell'entità o dell'elemento padre causa ilevering della relazione con tale entità/padre. L'operazione successiva dipende dal fatto che la relazione sia facoltativa o obbligatoria.

Relazioni facoltative

Per impostazione predefinita per le relazioni facoltative, il valore della chiave esterna è impostato su Null. Ciò significa che il dipendente/figlio non è più associato a un'entità o a un elemento padre. Ad esempio, caricare un blog e i post e quindi rimuovere uno dei post dal riquadro di spostamento della Blog.Posts raccolta:

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

Esaminando la visualizzazione debug del rilevamento delle modifiche dopo questa modifica, viene mostrato quanto segue:

  • L'FK Post.BlogId è stato impostato su Null (BlogId: <null> FK Modified Originally 1)
  • La Post.Blog navigazione di riferimento è stata impostata su Null (Blog: <null>)
  • Il post è stato rimosso dal Blog.Posts riquadro di spostamento della raccolta (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: []

Si noti che il post non è contrassegnato come Deleted. Viene contrassegnato come Modified in modo che il valore FK nel database venga impostato su null quando viene chiamato SaveChanges.

Relazioni obbligatorie

L'impostazione del valore FK su null non è consentita (e in genere non è possibile) per le relazioni necessarie. Di conseguenza, se si esegue una relazione obbligatoria, l'entità dipendente/figlio deve essere nuovamente padre o padre o rimossa dal database quando viene chiamato SaveChanges per evitare una violazione di vincolo referenziale. Questo comportamento è noto come "eliminazione di orfani" ed è il comportamento predefinito in EF Core per le relazioni necessarie.

Si supponga, ad esempio, di modificare la relazione tra blog e post in modo che sia necessario e quindi eseguire lo stesso codice dell'esempio precedente:

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

Esaminando la visualizzazione di debug dopo questa modifica, viene mostrato quanto segue:

  • Il post è stato contrassegnato come Deleted tale che verrà eliminato dal database quando viene chiamato SaveChanges.
  • La Post.Blog navigazione di riferimento è stata impostata su null (Blog: <null>).
  • Il post è stato rimosso dalla Blog.Posts navigazione raccolta (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: []

Si noti che l'oggetto Post.BlogId rimane invariato perché per una relazione obbligatoria non può essere impostato su Null.

La chiamata a SaveChanges comporta l'eliminazione del post orfano:

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

Eliminare i tempi orfani e ri-padre

Per impostazione predefinita, contrassegnare gli orfani non appena Deleted viene rilevata la modifica della relazione. Tuttavia, questo processo può essere ritardato fino a quando saveChanges non viene effettivamente chiamato. Ciò può essere utile per evitare di rendere orfani di entità che sono state rimosse da un'entità o da un padre, ma verranno ri-padre con un nuovo entità/padre prima di chiamare SaveChanges. ChangeTracker.DeleteOrphansTiming viene usato per impostare questo intervallo. Ad esempio:

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

Dopo aver rimosso il post dalla prima raccolta, l'oggetto non viene contrassegnato come Deleted nell'esempio precedente. EF Core sta invece verificando che la relazione venga interrotta anche se si tratta di una relazione obbligatoria. Il valore FK viene considerato null da EF Core anche se non può essere veramente Null perché il tipo non è nullable. Questo valore è noto come "null concettuale".

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: []

La chiamata a SaveChanges in questo momento comporta l'eliminazione del post orfano. Tuttavia, se come nell'esempio precedente, il post è associato a un nuovo blog prima di chiamare SaveChanges, verrà corretto in modo appropriato a quel nuovo blog e non è più considerato un orfano:

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 chiamato a questo punto aggiornerà il post nel database anziché eliminarlo.

È anche possibile disattivare l'eliminazione automatica degli orfani. Verrà generata un'eccezione se SaveChanges viene chiamato mentre viene rilevato un orfano. Ad esempio, questo codice:

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

Genererà questa eccezione:

System.InvalidOperationException: l'associazione tra entità 'Blog' e 'Post' con il valore chiave '{BlogId: 1}' è stata interrotta, ma la relazione è contrassegnata come obbligatoria o è implicitamente necessaria perché la chiave esterna non è nullable. Se l'entità dipendente/figlio deve essere eliminata quando viene interrotta una relazione obbligatoria, configurare la relazione per l'uso delle eliminazioni a catena.

L'eliminazione di orfani, nonché eliminazioni a catena, può essere forzata in qualsiasi momento chiamando ChangeTracker.CascadeChanges(). La combinazione di questa operazione con l'impostazione dell'intervallo di eliminazione orfano per Never garantire che gli orfani non vengano mai eliminati, a meno che EF Core non venga esplicitamente richiesto di farlo.

Modifica di una navigazione di riferimento

La modifica dello spostamento di riferimento di una relazione uno-a-molti ha lo stesso effetto della modifica dello spostamento della raccolta sull'altra estremità della relazione. L'impostazione dello spostamento di riferimento di dipendente/figlio su Null equivale a rimuovere l'entità dalla navigazione nella raccolta dell'entità principale/padre. Tutte le modifiche di correzione e database vengono apportate come descritto nella sezione precedente, inclusa la creazione di un orfano se la relazione è necessaria.

Relazioni uno-a-uno facoltative

Per le relazioni uno-a-uno, la modifica di un riquadro di spostamento dei riferimenti comporta la modifica di qualsiasi relazione precedente. Per le relazioni facoltative, questo significa che il valore FK nel figlio dipendente/figlio correlato in precedenza è impostato su Null. Ad esempio:

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 visualizzazione di debug prima di chiamare SaveChanges mostra che i nuovi asset hanno sostituito gli asset esistenti, contrassegnati come Modified con un valore FK 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>

Ciò comporta un aggiornamento e un inserimento quando viene chiamato SaveChanges:

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

Relazioni uno-a-uno obbligatorie

L'esecuzione dello stesso codice dell'esempio precedente, ma questa volta con una relazione uno-a-uno richiesta, mostra che l'oggetto associato BlogAssets in precedenza è ora contrassegnato come Deleted, poiché diventa un orfano quando il nuovo BlogAssets avviene:

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>

Viene quindi generato un'eliminazione e un inserimento quando viene chiamato SaveChanges:

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

L'intervallo di contrassegno degli orfani come eliminati può essere modificato nello stesso modo illustrato per gli spostamenti della raccolta e ha gli stessi effetti.

Eliminazione di un'entità

Relazioni facoltative

Quando un'entità viene contrassegnata come Deleted, ad esempio chiamando DbContext.Remove, i riferimenti all'entità eliminata vengono rimossi dagli spostamenti di altre entità. Per le relazioni facoltative, i valori FK nelle entità dipendenti vengono impostati su Null.

Ad esempio, contrassegnare il blog di Visual Studio come 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();

Esaminare la visualizzazione di debug dello strumento di rilevamento modifiche prima di chiamare SaveChanges:

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: []

Si noti che:

  • Il blog è contrassegnato come Deleted.
  • Gli asset correlati al blog eliminato hanno un valore FK null (BlogId: <null> FK Modified Originally 2) e una navigazione di riferimento Null (Blog: <null>)
  • Ogni post correlato al blog eliminato ha un valore FK null (BlogId: <null> FK Modified Originally 2) e una navigazione di riferimento Null (Blog: <null>)

Relazioni obbligatorie

Il comportamento di correzione per le relazioni obbligatorie è uguale a quello delle relazioni facoltative, ad eccezione del fatto che le entità dipendenti/figlio sono contrassegnate come Deleted poiché non possono esistere senza un'entità/padre e devono essere rimosse dal database quando SaveChanges viene chiamato per evitare un'eccezione di vincolo referenziale. Questa operazione è nota come "eliminazione a catena" ed è il comportamento predefinito in EF Core per le relazioni necessarie. Ad esempio, l'esecuzione dello stesso codice dell'esempio precedente ma con una relazione obbligatoria comporta la visualizzazione di debug seguente prima di chiamare SaveChanges:

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: []

Come previsto, i dipendenti/figli sono ora contrassegnati come Deleted. Si noti tuttavia che gli spostamenti sulle entità eliminate non sono stati modificati. Questo può sembrare strano, ma evita di eliminare completamente un grafico eliminato di entità cancellando tutti gli spostamenti. Ovvero, il blog, l'asset e i post formano ancora un grafico di entità anche dopo l'eliminazione. In questo modo è molto più semplice eliminare un grafico di entità rispetto al caso di EF6 in cui il grafo è stato shrededed.

Intervallo di eliminazione a catena e ri-padre

Per impostazione predefinita, l'eliminazione a catena viene eseguita non appena l'elemento padre/entità viene contrassegnato come Deleted. Questo vale per l'eliminazione di orfani, come descritto in precedenza. Come per l'eliminazione di orfani, questo processo può essere ritardato fino a quando saveChanges non viene chiamato o disabilitato completamente impostando ChangeTracker.CascadeDeleteTiming in modo appropriato. Ciò è utile nello stesso modo in cui è per eliminare gli orfani, incluso per la ri-genitorializzazione di elementi figlio/dipendenti dopo l'eliminazione di un'entità o di un elemento padre.

Le eliminazioni a catena, nonché l'eliminazione di orfani, possono essere forzate in qualsiasi momento chiamando ChangeTracker.CascadeChanges(). La combinazione di questa operazione con l'impostazione dell'intervallo di eliminazione a catena per Never garantisce che le eliminazioni a catena non vengano mai eseguite a meno che EF Core non venga esplicitamente richiesto di farlo.

Suggerimento

L'eliminazione a catena e l'eliminazione degli orfani sono strettamente correlate. Entrambi comportano l'eliminazione di entità dipendenti/figlio quando la relazione con l'entità principale/padre richiesta viene interrotta. Per l'eliminazione a catena, questo severing si verifica perché l'entità o l'elemento padre viene eliminato. Per gli orfani, l'entità principale/padre esiste ancora, ma non è più correlata alle entità dipendenti/figlio.

Relazioni molti-a-molti

Le relazioni molti-a-molti in EF Core vengono implementate usando un'entità join. Ogni lato della relazione molti-a-molti è correlato a questa entità di join con una relazione uno-a-molti. Questa entità join può essere definita e mappata in modo esplicito oppure può essere creata in modo implicito e nascosto. In entrambi i casi il comportamento sottostante è lo stesso. Questo comportamento sottostante verrà prima esaminato per comprendere il funzionamento del rilevamento delle relazioni molti-a-molti.

Funzionamento delle relazioni molti-a-molti

Si consideri questo modello di EF Core che crea una relazione molti-a-molti tra post e tag usando un tipo di entità join definito in modo esplicito:

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
}

Si noti che il PostTag tipo di entità join contiene due proprietà di chiave esterna. In questo modello, affinché un post sia correlato a un tag, deve essere presente un'entità join PostTag in cui il valore della PostTag.PostId chiave esterna corrisponde al valore della Post.Id chiave primaria e dove il valore della PostTag.TagId chiave esterna corrisponde al valore della Tag.Id chiave primaria. Ad esempio:

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

Esaminando la visualizzazione debug di Rilevamento modifiche dopo l'esecuzione di questo codice, il post e il tag sono correlati dalla nuova PostTag entità join:

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

Si noti che gli spostamenti della raccolta su Post e Tag sono stati corretti, come per gli spostamenti di riferimento in PostTag. Queste relazioni possono essere modificate dagli spostamenti anziché dai valori FK, come in tutti gli esempi precedenti. Ad esempio, il codice precedente può essere modificato per aggiungere la relazione impostando gli spostamenti di riferimento nell'entità join:

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

Ciò comporta esattamente la stessa modifica apportata agli FK e agli spostamenti dell'esempio precedente.

Ignorare gli spostamenti

La modifica manuale della tabella join può essere complessa. Le relazioni molti-a-molti possono essere modificate direttamente usando spostamenti di raccolte speciali che "ignorano" l'entità join. Ad esempio, due spostamenti skip possono essere aggiunti al modello precedente; uno da Post a Tag e l'altro da Tag a Post:

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
}

Questa relazione molti-a-molti richiede la configurazione seguente per garantire che gli spostamenti ignorati e gli spostamenti normali vengano usati tutti per la stessa relazione molti-a-molti:

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

Per altre informazioni sul mapping di relazioni molti-a-molti, vedere Relazioni .

Ignorare l'aspetto degli spostamenti e comportarsi come gli spostamenti di raccolte normali. Tuttavia, il modo in cui funzionano con i valori di chiave esterna è diverso. Associare un post a un tag, ma questa volta usando una navigazione skip:

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

Si noti che questo codice non usa l'entità join. Aggiunge invece un'entità a una raccolta di navigazione nello stesso modo in cui verrebbe eseguita se si trattasse di una relazione uno-a-molti. La visualizzazione di debug risultante è essenzialmente la stessa di prima:

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

Si noti che un'istanza dell'entità PostTag join è stata creata automaticamente con i valori FK impostati sui valori PK del tag e del post associati. Tutti gli spostamenti normali di riferimento e raccolta sono stati corretti in modo che corrispondano a questi valori FK. Inoltre, poiché questo modello contiene spostamenti ignorati, questi sono stati corretti anche. In particolare, anche se è stato aggiunto il tag alla Post.Tags navigazione skip, anche l'inverso Tag.Posts ignora lo spostamento sull'altro lato di questa relazione è stato corretto anche per contenere il post associato.

Vale la pena notare che le relazioni molti-a-molti sottostanti possono comunque essere modificate direttamente anche quando gli spostamenti ignorati sono stati sovrapposti in alto. Ad esempio, il tag e Post possono essere associati come abbiamo fatto prima di introdurre gli spostamenti ignorati:

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

Oppure usando i valori FK:

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

Ciò continuerà a comportare la corretta correzione degli spostamenti ignorando la visualizzazione, con lo stesso output della visualizzazione di debug dell'esempio precedente.

Ignorare solo gli spostamenti

Nella sezione precedente sono state aggiunte operazioni di spostamento ignorate oltre a definire completamente le due relazioni uno-a-molti sottostanti. Ciò è utile per illustrare cosa accade ai valori FK, ma spesso non è necessario. È invece possibile definire la relazione molti-a-molti usando solo gli spostamenti ignorati. Questo è il modo in cui la relazione molti-a-molti è definita nel modello nella parte superiore di questo documento. Usando questo modello, è possibile associare nuovamente un post e un tag aggiungendo un post alla Tag.Posts navigazione skip (o, in alternativa, aggiungendo un tag alla Post.Tags navigazione skip):

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

Esaminando la visualizzazione di debug dopo aver apportato questa modifica, EF Core ha creato un'istanza di Dictionary<string, object> per rappresentare l'entità join. Questa entità join contiene proprietà di PostsId chiave esterna e TagsId che sono state impostate in modo che corrispondano ai valori PK del post e del tag associati.

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

Per altre informazioni sulle entità di join implicite e sull'uso dei tipi di Dictionary<string, object> entità, vedere Relazioni.

Importante

Il tipo CLR usato per i tipi di entità join per convenzione può cambiare nelle versioni future per migliorare le prestazioni. Non dipendere dal tipo di Dictionary<string, object> join a meno che non sia stato configurato in modo esplicito.

Unire entità con payload

Finora tutti gli esempi hanno usato un tipo di entità join (esplicito o implicito) che contiene solo le due proprietà di chiave esterna necessarie per la relazione molti-a-molti. Nessuno di questi valori FK deve essere impostato in modo esplicito dall'applicazione durante la modifica delle relazioni perché i relativi valori provengono dalle proprietà della chiave primaria delle entità correlate. Ciò consente a EF Core di creare istanze dell'entità di join senza dati mancanti.

Payload con valori generati

EF Core supporta l'aggiunta di proprietà aggiuntive al tipo di entità join. Questa operazione è nota come dare all'entità join un "payload". Ad esempio, aggiungere TaggedOn la proprietà all'entità PostTag join:

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
}

Questa proprietà payload non verrà impostata quando EF Core crea un'istanza dell'entità join. Il modo più comune per gestire questa operazione consiste nell'usare le proprietà del payload con valori generati automaticamente. Ad esempio, la TaggedOn proprietà può essere configurata per l'uso di un timestamp generato dall'archivio quando viene inserita ogni nuova entità:

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 post può ora essere contrassegnato nello stesso modo di prima:

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

Esaminando la visualizzazione debug di Rilevamento modifiche dopo aver chiamato SaveChanges, la proprietà payload è stata impostata in modo appropriato:

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

Impostazione esplicita dei valori del payload

Seguendo l'esempio precedente, si aggiungerà una proprietà payload che non usa un valore generato automaticamente:

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 post può ora essere contrassegnato nello stesso modo di prima e l'entità join verrà comunque creata automaticamente. È quindi possibile accedere a questa entità usando uno dei meccanismi descritti in Accesso alle entità rilevate. Ad esempio, il codice seguente usa DbSet<TEntity>.Find per accedere all'istanza dell'entità join:

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

Dopo aver individuato l'entità join, è possibile modificarla nel modo normale, in questo esempio, per impostare la TaggedBy proprietà payload prima di chiamare SaveChanges.

Nota

Si noti che qui è necessaria una chiamata a ChangeTracker.DetectChanges() per consentire a EF Core di rilevare la modifica della proprietà di navigazione e creare l'istanza dell'entità join prima Find di essere usata. Per altre informazioni, vedere Rilevamento modifiche e notifiche .

In alternativa, l'entità join può essere creata in modo esplicito per associare un post a un tag. Ad esempio:

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

Infine, un altro modo per impostare i dati di payload consiste nell'eseguire l'override SaveChanges o usare l'evento DbContext.SavingChanges per elaborare le entità prima di aggiornare il database. Ad esempio:

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

    return base.SaveChanges();
}