Entità disconnesse

Un'istanza di DbContext sottoporrà automaticamente a rilevamento delle modifiche le entità restituite dal database. Le modifiche apportate a queste entità verranno quindi rilevate quando viene chiamato SaveChanges e il database verrà aggiornato in base alle esigenze. Vedere Salvataggio di base e Dati correlati per informazioni dettagliate.

Tuttavia, a volte le entità vengono sottoposte a query usando un'istanza di contesto e poi salvate con un'istanza diversa. Questo accade spesso in scenari "disconnessi", ad esempio un'applicazione Web in cui le entità vengono recuperate tramite query, inviate al client, modificate, inviate al server in una richiesta e quindi salvate. In questo caso, la seconda istanza del contesto deve sapere se le entità sono nuove (devono essere inserite) o esistenti (devono essere aggiornate).

Suggerimento

È possibile visualizzare l'esempio di questo articolo in GitHub.

Suggerimento

EF Core può eseguire il rilevamento delle modifiche per una sola istanza di qualsiasi entità con un determinato valore di chiave primaria. Il modo migliore per evitare che ciò diventi un problema consiste nell'usare un contesto di breve durata per ogni unità di lavoro, in modo che il contesto sia inizialmente vuoto, abbia entità collegate, salvi queste entità, per poi essere eliminato e rimosso.

Identificazione di nuove entità

Nuove identità identificate dal client

Il caso più semplice da affrontare è quando il client comunica al server se l'entità è nuova o esistente. Ad esempio, spesso la richiesta di inserire una nuova entità è diversa dalla richiesta di aggiornare un'entità esistente.

Nella parte restante di questa sezione vengono illustrati i casi in cui è necessario determinare in altro modo se inserire o aggiornare.

Con chiavi generate automaticamente

Il valore di una chiave generata automaticamente può essere spesso usato per determinare se un'entità deve essere inserita o aggiornata. Se la chiave non è stata impostata (ad esempio, ha ancora il valore predefinito di CLR Null, zero e così via), l'entità deve essere nuova e quindi inserita. D'altra parte, se il valore della chiave è stato impostato, l'entità deve essere già stata salvata in precedenza e ora richiede un aggiornamento. In altre parole, se la chiave ha un valore, allora l'entità è stata già sottoposta a query e inviata al client e ora ritorna per l'aggiornamento.

È facile verificare la presenza di una chiave non impostata quando è noto il tipo di entità:

public static bool IsItNew(Blog blog)
    => blog.BlogId == 0;

Tuttavia, EF include anche un modo predefinito per eseguire questa operazione per qualsiasi tipo di entità e tipo di chiave:

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

Suggerimento

Le chiavi vengono impostate non appena le entità vengono incluse nel rilevamento delle modifiche dal contesto, anche se l'entità risulta con lo stato Added. Ciò è utile durante l'attraversamento di un grafo di entità e per decidere come procedere con ognuna, ad esempio quando di usa l'API TrackGraph. Il valore della chiave deve essere usato solo nel modo illustrato di seguito prima che venga effettuata qualsiasi chiamata per il rilevamento delle modifiche dell'entità.

Con altre chiavi

È necessario un altro meccanismo per identificare le nuove entità quando i valori di chiave non vengono generati automaticamente. Esistono due approcci generali a questo scopo:

  • Recuperare l'entità tramite query
  • Passare un flag dal client

Per eseguire una query per l'entità, è sufficiente usare il metodo Find:

public static bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

Esula dagli scopi di questo documento mostrare il codice completo per il passaggio di un flag da un client. In un'app Web, in genere significa effettuare richieste diverse per azioni diverse oppure passare uno stato nella richiesta e quindi estrarlo nel controller.

Salvataggio di singole entità

Quando è noto se è necessario eseguire un inserimento o un aggiornamento, è possibile usare in modo appropriato Add o Update:

public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}

public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

Tuttavia, se l'entità usa valori di chiave generati automaticamente, è possibile usare il metodo Update per entrambi i casi:

public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

Il metodo Update contrassegna in genere l'entità per l'aggiornamento e non per l'inserimento. Tuttavia, se l'entità ha una chiave generata automaticamente e non è stato impostato alcun valore di chiave, l'entità viene invece contrassegnata automaticamente per l'inserimento.

Se l'entità non usa chiavi generate automaticamente, l'applicazione deve quindi decidere se l'entità deve essere inserita o aggiornata. Ad esempio:

public static void InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs.Find(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    context.SaveChanges();
}

La procedura è la seguente:

  • Se Find restituisce Null, il database non contiene già il blog con tale ID, pertanto si chiama Add per contrassegnarlo per l'inserimento.
  • Se Find restituisce un'entità, esiste nel database e il contesto ora monitora l'entità esistente
    • È quindi possibile usare SetValues per impostare i valori per tutte le proprietà dell'entità sui valori provenienti dal client.
    • La chiamata di SetValues contrassegnerà l'entità per l'aggiornamento in base alle esigenze.

Suggerimento

SetValues contrassegnerà come modificate solo le proprietà con valori diversi da quelli nell'entità con rilevamento delle modifiche. Questo significa che quando viene inviato l'aggiornamento, verranno aggiornate solo le colonne effettivamente modificate. (In assenza di modifiche non verrà inviato alcun aggiornamento.)

Utilizzo dei grafi

Risoluzione di identità

Come indicato in precedenza, EF Core può eseguire il rilevamento delle modifiche per una sola istanza di qualsiasi entità con un determinato valore di chiave primaria. Quando si utilizzano i grafi, il grafo deve essere creato idealmente in modo da mantenere questa invariante e da usare il contesto per una sola unità di lavoro. Se il grafo contiene duplicati, sarà necessario elaborare il grafo prima di inviarlo a EF per consolidare più istanze in una unica. Questa operazione potrebbe non essere semplice se le istanze hanno valori e relazioni in conflitto, quindi è consigliabile eseguire il consolidamento dei duplicati non appena possibile nella pipeline dell'applicazione per evitare la risoluzione dei conflitti.

Tutte le entità nuove/esistenti

Un esempio di utilizzo dei grafi è l'inserimento o l'aggiornamento di un blog con la raccolta di post associati. Se tutte le entità nel grafo devono essere inserite o devono essere tutte aggiornate, il processo è identico a quello sopra descritto per singole entità. Ad esempio, un grafo di blog e post creato come il seguente:

var blog = new Blog
{
    Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};

può essere inserito nel modo seguente:

public static void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

La chiamata di Add contrassegnerà il blog e tutti i post per l'inserimento.

Analogamente, se tutte le entità in un grafo devono essere aggiornate, si può usare Update:

public static void UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

Il blog e tutti i relativi post verranno contrassegnati per l'aggiornamento.

Combinazione di entità nuove ed esistenti

Con le chiavi generate automaticamente, è possibile usare Update sia per gli inserimenti che per gli aggiornamenti, anche se il grafo contiene una combinazione di entità che richiedono l'inserimento e che richiedono l'aggiornamento:

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

Update contrassegnerà qualsiasi entità nel grafo, blog o post, per l'inserimento, se non dispone di valore di chiave impostato, mentre tutte le altre entità verranno contrassegnate per l'aggiornamento.

Come prima, se non si usano chiavi generate automaticamente, è possibile usare una query e alcune operazioni di elaborazione:

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }
    }

    context.SaveChanges();
}

Gestione delle eliminazioni

L'eliminazione può essere difficile da gestire, dato che l'assenza di un'entità indica spesso che deve essere eliminata. Un modo per risolvere questo problema consiste nell'usare "eliminazioni temporanee" in modo che l'entità venga contrassegnata come eliminata anziché essere effettivamente eliminata. Le eliminazioni diventano quindi uguali agli aggiornamenti. Le eliminazioni software possono essere implementate usando filtri di query.

Per le vere eliminazioni, un modello comune consiste nell'usare un'estensione del modello di query per eseguire essenzialmente un confronto delle differenze del grafo. Ad esempio:

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in existingBlog.Posts)
        {
            if (!blog.Posts.Any(p => p.PostId == post.PostId))
            {
                context.Remove(post);
            }
        }
    }

    context.SaveChanges();
}

TrackGraph

Internamente, Add, Attach e Update usano l'attraversamento del grafo determinando per ogni entità se deve essere contrassegnata come Added (per l'inserimento), Modified (per l'aggiornamento), Unchanged (per non eseguire alcuna operazione) o Delete (per l'eliminazione). Questo meccanismo viene esposto tramite l'API TrackGraph. Ad esempio, si supponga che quando il client invia un grafo delle entità imposti alcuni flag per ogni entità per indicare come deve essere gestita. TrackGraph può quindi essere usato per elaborare questo flag:

public static void SaveAnnotatedGraph(DbContext context, object rootEntity)
{
    context.ChangeTracker.TrackGraph(
        rootEntity,
        n =>
        {
            var entity = (EntityBase)n.Entry.Entity;
            n.Entry.State = entity.IsNew
                ? EntityState.Added
                : entity.IsChanged
                    ? EntityState.Modified
                    : entity.IsDeleted
                        ? EntityState.Deleted
                        : EntityState.Unchanged;
        });

    context.SaveChanges();
}

I flag vengono visualizzati solo come parte dell'entità per semplicità dell'esempio. In genere, i flag farebbero parte di un DTO o qualche altro stato incluso nella richiesta.