Funzionalità aggiuntive di Rilevamento modifiche

Questo documento illustra varie funzionalità e scenari che coinvolgono il rilevamento delle modifiche.

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 in questo documento scaricando il codice di esempio da GitHub.

Add Contro AddAsync

Entity Framework Core (EF Core) fornisce metodi asincroni ogni volta che si usa tale metodo può causare un'interazione con il database. I metodi sincroni vengono forniti anche per evitare il sovraccarico quando si usano database che non supportano l'accesso asincrono a prestazioni elevate.

DbContext.Add e DbSet<TEntity>.Add non accedono normalmente al database, poiché questi metodi iniziano intrinsecamente a tenere traccia delle entità. Tuttavia, alcune forme di generazione di valori possono accedere al database per generare un valore chiave. L'unico generatore di valori che esegue questa operazione e viene fornito con EF Core è HiLoValueGenerator<TValue>. L'uso di questo generatore è insolito; non è mai configurato per impostazione predefinita. Ciò significa che la maggior parte delle applicazioni deve usare Adde non AddAsync.

Altri metodi simili come Update, Attache Remove non dispongono di overload asincroni perché non generano mai nuovi valori chiave e quindi non devono mai accedere al database.

AddRange, UpdateRange, AttachRange e RemoveRange

DbSet<TEntity> e DbContext fornire versioni alternative di Add, Update, Attache Remove che accettano più istanze in una singola chiamata. Questi metodi sono AddRange, UpdateRange, AttachRangee RemoveRange rispettivamente.

Questi metodi vengono forniti come praticità. L'uso di un metodo "range" ha la stessa funzionalità di più chiamate al metodo equivalente non intervallo. Non esiste alcuna differenza significativa di prestazioni tra i due approcci.

Nota

Questo è diverso da EF6, dove AddRange e Add entrambi chiamati DetectChangesautomaticamente , ma chiamando Add più volte RilevaChanges deve essere chiamato più volte anziché una sola volta. Questo ha reso AddRange più efficiente ef6. In EF Core nessuno di questi metodi chiama DetectChangesautomaticamente .

Metodi DbContext e DbSet

Molti metodi, tra cui Add, AttachUpdate, e Remove, hanno implementazioni in entrambi DbSet<TEntity> e DbContext. Questi metodi hanno esattamente lo stesso comportamento per i tipi di entità normali. Questo perché il tipo CLR dell'entità viene mappato a uno e un solo tipo di entità nel modello EF Core. Pertanto, il tipo CLR definisce completamente la posizione in cui l'entità si adatta al modello e quindi dbSet da usare può essere determinata in modo implicito.

L'eccezione a questa regola è quando si usano tipi di entità di tipo condiviso, introdotti in EF Core 5.0, principalmente per le entità join molti-a-molti. Quando si usa un tipo di entità di tipo condiviso, è necessario prima creare un oggetto DbSet per il tipo di modello EF Core usato. I metodi come Add, Update, Attache Remove possono quindi essere usati in DbSet senza ambiguità in base al tipo di modello EF Core usato.

I tipi di entità di tipo condiviso vengono usati per impostazione predefinita per le entità join nelle relazioni molti-a-molti. Un tipo di entità di tipo condiviso può anche essere configurato in modo esplicito per l'uso in una relazione molti-a-molti. Ad esempio, il codice seguente configura Dictionary<string, int> come tipo di entità join:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

La modifica delle chiavi esterne e degli spostamenti illustra come associare due entità tracciando una nuova istanza di entità join. Il codice seguente esegue questa operazione per il Dictionary<string, int> tipo di entità di tipo condiviso usato per l'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);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

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

context.SaveChanges();

Si noti che DbContext.Set<TEntity>(String) viene usato per creare un oggetto DbSet per il tipo di PostTag entità. Questo oggetto DbSet può quindi essere usato per chiamare Add con la nuova istanza dell'entità join.

Importante

Il tipo CLR usato per i tipi di entità join per convenzione può cambiare nelle versioni future per migliorare le prestazioni. Non dipende da alcun tipo di entità join specifico, a meno che non sia Dictionary<string, int> stato configurato in modo esplicito come fatto nel codice precedente.

Proprietà e accesso al campo

L'accesso alle proprietà dell'entità usa il campo di backup della proprietà per impostazione predefinita. Questo è efficiente ed evita l'attivazione di effetti collaterali dalla chiamata di getters e setters della proprietà. Ad esempio, questo è il modo in cui il caricamento lazy è in grado di evitare l'attivazione di cicli infiniti. Per altre informazioni sulla configurazione dei campi di backup nel modello, vedere Campi di backup .

A volte può essere consigliabile che EF Core generi effetti collaterali quando modifica i valori delle proprietà. Ad esempio, quando il data binding alle entità, l'impostazione di una proprietà può generare notifiche agli Stati Uniti, che non si verificano quando si imposta il campo direttamente. Questa operazione può essere ottenuta modificando l'oggetto PropertyAccessMode per:

Le modalità Field di accesso alle proprietà e PreferField causeranno l'accesso a EF Core al valore della proprietà tramite il relativo campo di backup. Analogamente, PropertyPreferProperty ef Core accede al valore della proprietà tramite il relativo getter e setter.

Se Field o Property vengono usati e EF Core non può accedere al valore tramite il campo o la proprietà getter/setter rispettivamente, EF Core genererà un'eccezione. Ciò garantisce che EF Core usi sempre l'accesso ai campi/proprietà quando si ritiene che sia.

D'altra parte, le PreferField modalità e PreferProperty torneranno a usare rispettivamente la proprietà o il campo di backup se non è possibile usare l'accesso preferito. PreferField è l'impostazione predefinita. Ciò significa che EF Core userà i campi ogni volta che può, ma non avrà esito negativo se una proprietà deve essere accessibile tramite il relativo getter o setter.

FieldDuringConstruction e PreferFieldDuringConstruction configurare EF Core per l'uso dei campi di backup solo quando si creano istanze di entità. Ciò consente l'esecuzione di query senza effetti collaterali getter e setter, mentre le modifiche successive delle proprietà di EF Core causeranno questi effetti collaterali.

Le diverse modalità di accesso alle proprietà sono riepilogate nella tabella seguente:

PropertyAccessMode Preferenza Preferenza per la creazione di entità Fallback Fallback creazione di entità
Field Campo Campo Genera un'eccezione Genera un'eccezione
Property Proprietà Proprietà Genera un'eccezione Genera un'eccezione
PreferField Campo Campo Proprietà Proprietà
PreferProperty Proprietà Proprietà Campo Campo
FieldDuringConstruction Proprietà Campo Campo Genera un'eccezione
PreferFieldDuringConstruction Proprietà Campo Campo Proprietà

Valori temporanei

EF Core crea valori chiave temporanei durante il rilevamento di nuove entità che avranno valori chiave reali generati dal database quando viene chiamato SaveChanges. Per una panoramica di come vengono usati questi valori temporanei, vedere Rilevamento modifiche in EF Core.

Accesso ai valori temporanei

A partire da EF Core 3.0, i valori temporanei vengono archiviati nel tracker delle modifiche e non vengono impostati direttamente sulle istanze dell'entità. Tuttavia, questi valori temporanei vengono esposti quando si usano i vari meccanismi per l'accesso alle entità tracciate. Ad esempio, il codice seguente accede a un valore temporaneo usando EntityEntry.CurrentValues:

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

L'output di questo codice è:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary può essere usato per verificare la presenza di valori temporanei.

Modifica dei valori temporanei

A volte è utile usare in modo esplicito i valori temporanei. Ad esempio, una raccolta di nuove entità potrebbe essere creata in un client Web e quindi serializzata nel server. I valori chiave esterna sono un modo per configurare le relazioni tra queste entità. Il codice seguente usa questo approccio per associare un grafico di nuove entità in base a chiave esterna, consentendo comunque la generazione di valori chiave reali quando viene chiamato SaveChanges.

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -1,
        Title = "Announcing the Release of EF Core 5.0",
        Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
    },
    new Post
    {
        Id = -2,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

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

context.SaveChanges();

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

Si noti che:

  • I numeri negativi vengono usati come valori di chiave temporanea; questo non è obbligatorio, ma è una convenzione comune per evitare conflitti chiave.
  • La Post.BlogId proprietà FK viene assegnata allo stesso valore negativo dell'pk del blog associato.
  • I valori PK vengono contrassegnati come temporanei impostando IsTemporary dopo che ogni entità viene monitorata. Ciò è necessario perché si presuppone che qualsiasi valore della chiave fornito dall'applicazione sia un valore di chiave reale.

L'analisi della visualizzazione debug del rilevamento modifiche prima di chiamare SaveChanges mostra che i valori PK sono contrassegnati come temporanei e post sono associati ai blog corretti, incluse le correzioni degli spostamenti:

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  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: -1} Added
  Id: -1 PK Temporary
  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}

Dopo aver chiamato SaveChanges, questi valori temporanei sono stati sostituiti da valori reali generati dal database:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{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: 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: []

Uso dei valori predefiniti

EF Core consente a una proprietà di ottenere il valore predefinito dal database quando SaveChanges viene chiamato. Analogamente ai valori chiave generati, EF Core userà solo un valore predefinito dal database se non è stato impostato in modo esplicito alcun valore. Si consideri ad esempio il tipo di entità seguente:

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

La ValidFrom proprietà è configurata per ottenere un valore predefinito dal database:

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

Quando si inserisce un'entità di questo tipo, EF Core consente al database di generare il valore a meno che non sia stato impostato un valore esplicito. Ad esempio:

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

context.SaveChanges();

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

L'analisi della visualizzazione debug del rilevamento modifiche mostra che il primo token è ValidFrom stato generato dal database, mentre il secondo token ha usato il valore impostato in modo esplicito:

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

Nota

L'uso dei valori predefiniti del database richiede che la colonna del database disponga di un vincolo valore predefinito configurato. Questa operazione viene eseguita automaticamente dalle migrazioni di EF Core quando si usa HasDefaultValueSql o HasDefaultValue. Assicurarsi di creare il vincolo predefinito sulla colonna in un altro modo quando non si usano le migrazioni di EF Core.

Uso di proprietà nullable

EF Core è in grado di determinare se è stata impostata o meno una proprietà confrontando il valore della proprietà con il valore predefinito CLR per il tipo. Ciò funziona correttamente nella maggior parte dei casi, ma significa che il valore predefinito CLR non può essere inserito in modo esplicito nel database. Si consideri ad esempio un'entità con una proprietà integer:

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

Dove tale proprietà è configurata per avere un database predefinito di -1:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

L'intenzione è che l'impostazione predefinita di -1 verrà usata ogni volta che non è impostato un valore esplicito. Tuttavia, l'impostazione del valore su 0 (l'impostazione predefinita CLR per gli interi) è indistinguishable su EF Core non impostando alcun valore, ciò significa che non è possibile inserire 0 per questa proprietà. Ad esempio:

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Si noti che l'istanza in cui Count è stata impostata in modo esplicito su 0 ottiene comunque il valore predefinito dal database, che non è quello previsto. Un modo semplice per gestire questa operazione consiste nel rendere la Count proprietà nullable:

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

Ciò rende il valore predefinito CLR null, anziché 0, che significa che 0 verrà ora inserito quando viene impostato in modo esplicito:

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Uso di campi di backup nullable

Nota

Questo modello di campo di backing nullable è supportato da EF Core 5.0 e versioni successive.

Problema di rendere nullable la proprietà che potrebbe non essere concettualmente nullable nel modello di dominio. Forzando la proprietà per essere nullable, pertanto il modello viene compromesso.

A partire da EF Core 5.0, la proprietà può essere lasciata non nullable, con solo il campo di backup che è nullable. Ad esempio:

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

Ciò consente di inserire il valore predefinito CLR (0) se la proprietà è impostata in modo esplicito su 0, mentre non è necessario esporre la proprietà come nullable nel modello di dominio. Ad esempio:

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Campi di backup nullable per le proprietà bool

Questo modello è particolarmente utile quando si usano proprietà bool con le impostazioni predefinite generate dall'archivio. Poiché il valore predefinito CLR per bool è "false", significa che "false" non può essere inserito in modo esplicito usando il modello normale. Si consideri ad esempio un User tipo di entità:

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

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

La IsAuthorized proprietà è configurata con un valore predefinito del database "true":

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

La IsAuthorized proprietà può essere impostata su "true" o "false" in modo esplicito prima dell'inserimento o può essere lasciata non impostata nel caso in cui verrà usato il valore predefinito del database:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

context.SaveChanges();

L'output di SaveChanges quando si usa SQLite mostra che il valore predefinito del database viene usato per Mac, mentre i valori espliciti vengono impostati per Alice e Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Solo impostazioni predefinite dello schema

A volte è utile avere impostazioni predefinite nello schema di database creato dalle migrazioni di EF Core senza EF Core che usano mai questi valori per gli inserimenti. Questa operazione può essere ottenuta configurando la proprietà, ad PropertyBuilder.ValueGeneratedNever esempio:

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();