Rilevamento delle modifiche in EF Core

Ogni istanza di DbContext tiene traccia delle modifiche apportate alle entità. Queste entità rilevate a loro volta determinano le modifiche apportate al database quando viene chiamato SaveChanges.

Questo documento presenta una panoramica del rilevamento delle modifiche di Entity Framework Core (EF Core) e della relativa correlazione con query e aggiornamenti.

Suggerimento

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

Suggerimento

Per semplicità, questo documento usa e fa riferimento a metodi sincroni, ad esempio SaveChanges, anziché i relativi equivalenti asincroni, ad esempio SaveChangesAsync. La chiamata e l'attesa del metodo asincrono possono essere sostituite se non diversamente specificato.

Come rilevare le entità

Le istanze di entità vengono rilevate quando sono:

  • Restituite da una query eseguita sul database
  • Collegate in modo esplicito a DbContext da Add, Attach, Updateo metodi simili
  • Rilevate come nuove entità connesse alle entità rilevate esistenti

Le istanze di entità non vengono più rilevate quando:

  • DbContext viene eliminato
  • Lo strumento di rilevamento delle modifiche viene cancellato
  • Le entità vengono scollegate in modo esplicito

DbContext è progettato per rappresentare un'unità di lavoro di breve durata, come descritto in Inizializzazione e configurazione di DbContext. Ciò significa che l'eliminazione di DbContext è il modo normale per interrompere il rilevamento delle entità. In altre parole, la durata di un oggetto DbContext deve essere:

  1. Creare l'istanza di DbContext
  2. Rilevare alcune entità
  3. Apportare alcune modifiche alle entità
  4. Chiamare SaveChanges per aggiornare il database
  5. Eliminare l'istanza di DbContext

Suggerimento

Non è necessario cancellare lo strumento di rilevamento delle modifiche o scollegare in modo esplicito le istanze di entità quando si usa questo approccio. Se è necessario scollegare le entità, la chiamata di ChangeTracker.Clear risulta tuttavia più efficiente rispetto allo scollegamento di un'entità alla volta.

Stati entità

Ogni entità è associata a un determinato EntityState:

  • Le entità Detached non vengono rilevate da DbContext.
  • Le entità Added sono nuove e non sono state ancora inserite nel database. Ciò significa che verranno inserite quando viene chiamato SaveChanges.
  • Le entità Unchangednon sono state modificate dopo essere state sottoposte a query dal database. Tutte le entità restituite dalle query hanno inizialmente questo stato.
  • Le entità Modified sono state modificate dopo essere state sottoposte a query dal database. Ciò significa che verranno aggiornate quando viene chiamato SaveChanges.
  • Le entità Deleted esistono nel database, ma vengono contrassegnate per essere eliminate quando viene chiamato SaveChanges.

EF Core rileva le modifiche a livello di proprietà. Se ad esempio viene modificato solo un singolo valore di proprietà, un aggiornamento del database modificherà solo tale valore. Le proprietà possono essere tuttavia contrassegnate come modificate solo quando l'entità stessa si trova nello stato Modificato. In alternativa, da un punto di vista alternativo, lo stato Modificato indica che almeno un valore della proprietà è stato contrassegnato come modificato.

Nella tabella seguente sono riepilogati i diversi stati:

Stato entità Rilevato da DbContext Esistente nel database Proprietà modificate Azione su SaveChanges
Detached No - - -
Added No - Inserisci
Unchanged No -
Modified Aggiornamento
Deleted - Eliminazione

Nota

Questo testo usa i termini dei database relazionali per maggiore chiarezza. I database NoSQL supportano in genere operazioni simili, ma possibilmente con nomi diversi. Per altre informazioni, vedere la documentazione del provider di database.

Rilevamento da query

Il rilevamento delle modifiche di EF Core funziona meglio quando la stessa istanza di DbContext viene usata per eseguire query per le entità e aggiornarle chiamando SaveChanges. Ciò dipende dal fatto che EF Core rileva automaticamente lo stato delle entità sottoposte a query e quindi rileva le eventuali modifiche apportate a queste entità quando viene chiamato SaveChanges.

Questo approccio presenta diversi vantaggi rispetto al rilevamento esplicito delle istanze di entità:

  • È semplice. Gli stati delle entità devono essere modificati raramente in modo esplicito: EF Core gestisce le modifiche dello stato.
  • Gli aggiornamenti sono limitati solo ai valori effettivamente modificati.
  • I valori delle proprietà shadow vengono mantenuti e usati in base alle esigenze. Ciò risulta particolarmente rilevante quando le chiavi esterne vengono archiviate nello stato shadow.
  • I valori originali delle proprietà vengono mantenuti automaticamente e usati per aggiornamenti efficienti.

Query e aggiornamento semplici

Si consideri ad esempio un modello di blog/post semplice:

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

    public IList<Post> Posts { get; } = new List<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; }
}

È possibile usare questo modello per eseguire query su blog e post e quindi apportare alcuni aggiornamenti al database:

using var context = new BlogsContext();

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

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

La chiamata a SaveChanges comporta gli aggiornamenti del database seguenti, con SQLite come database di esempio:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

La visualizzazione del debug del rilevamento modifiche è un ottimo modo per visualizzare quali entità vengono rilevati e quali sono i relativi stati. L'inserimento ad esempio del codice seguente nell'esempio precedente prima di chiamare SaveChanges:

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

Genera l'output seguente:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
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}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

Si noti in particolare:

  • La proprietà Blog.Name viene contrassegnata come modificata (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog') e come risultato il blog avrà lo stato Modified.
  • La proprietà Post.Title del post 2 viene contrassegnata come modificata (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5') e come risultato il post avrà lo stato Modified.
  • Gli altri valori delle proprietà del post 2 non sono stati modificati e pertanto non vengono contrassegnati come modificati. Questi valori non sono quindi inclusi nell'aggiornamento del database.
  • L'altro post non è stato modificato in alcun modo. Questo è il motivo per cui ha ancora lo stato Unchanged e non è incluso nell'aggiornamento del database.

Eseguire query e quindi inserire, aggiornare ed eliminare

Gli aggiornamenti come quelli dell'esempio precedente possono essere combinati con inserimenti ed eliminazioni nella stessa unità di lavoro. Ad esempio:

using var context = new BlogsContext();

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

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

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

context.SaveChanges();

In questo esempio:

  • Un blog e i post correlati vengono sottoposti a query dal database e rilevati
  • La proprietà Blog.Name viene modificata
  • Un nuovo post viene aggiunto alla raccolta di post esistenti per il blog
  • Un post esistente è contrassegnato per l'eliminazione chiamando DbContext.Remove

Se si esamina di nuovo la visualizzazione del debug del rilevamento modifiche prima di chiamare SaveChanges verrà illustrato in che modo EF Core sta rilevando queste modifiche:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {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}
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: {Id: 1}

Si noti che:

  • Il blog è contrassegnato come Modified. Verrà generato un aggiornamento del database.
  • Il post 2 è contrassegnato come Deleted. Verrà generata un'eliminazione del database.
  • Un nuovo post con un ID temporaneo è associato al blog 1 ed è contrassegnato come Added. Verrà generato un inserimento del database.

Si otterranno i comandi di database seguenti (con SQLite) quando viene chiamato SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

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

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Per altre informazioni sull'inserimento e l'eliminazione di entità, vedere Rilevamento esplicito delle entità. Per altre informazioni sul modo in cui EF Core rileva automaticamente le modifiche in base a questa procedura, vedere Rilevamento modifiche e notifiche.

Suggerimento

Chiamare ChangeTracker.HasChanges() per determinare se sono state apportate modifiche a causa delle quali SaveChanges apporterà aggiornamenti al database. Se HasChanges restituisce false, SaveChanges sarà no-op.