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

Add e AddAsync

Entity Framework Core (EF Core) fornisce metodi asincroni ogni volta che si usa tale metodo può comportare un'interazione del database. Vengono inoltre forniti metodi sincroni per evitare sovraccarichi quando si usano database che non supportano l'accesso asincrono ad alte prestazioni.

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 di chiave. L'unico generatore di valori che esegue questa operazione e viene fornito con EF Core è HiLoValueGenerator<TValue>. L'uso di questo generatore non è comune; non è mai configurato per impostazione predefinita. Ciò significa che la maggior parte delle applicazioni deve usare Adde non AddAsync.

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

AddRange, UpdateRange, AttachRange e RemoveRange

DbSet<TEntity> e DbContext forniscono versioni alternative di Add, Update, Attache Remove che accettano più istanze in una singola chiamata. Questi metodi sono AddRangerispettivamente , AttachRangeUpdateRange, e RemoveRange .

Questi metodi vengono forniti per 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 comportamento è diverso da EF6, dove AddRange e entrambi chiamati DetectChangesautomaticamente , ma la chiamata Add più volte ha causato la chiamata di DetectChanges più volte anziché Add una sola volta. Ciò 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, includono implementazioni sia DbSet<TEntity> in che DbContextin . Questi metodi hanno esattamente lo stesso comportamento per i tipi di entità normali. Questo perché il tipo CLR dell'entità viene mappato a un solo tipo di entità nel modello EF Core. Di conseguenza, il tipo CLR definisce completamente la posizione in cui l'entità si inserisce nel modello e quindi il DbSet da usare può essere determinato in modo implicito.

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

I tipi di entità di tipo condiviso vengono usati per impostazione predefinita per le entità di join in 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 viene configurato 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 di chiavi esterne e spostamenti mostra come associare due entità tenendo traccia di 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 PostTag tipo di entità. Questo 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 dipendere da alcun tipo di entità join specifico, a meno che non sia stato configurato in modo esplicito come è fatto nel Dictionary<string, int> codice precedente.

Proprietà e accesso ai campi

Per impostazione predefinita, l'accesso alle proprietà dell'entità usa il campo sottostante della proprietà. Questo è efficiente ed evita di attivare effetti collaterali dalla chiamata di getter e setter della proprietà. Ad esempio, questo è il modo in cui il caricamento differita è 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 preferibile che EF Core generi effetti collaterali quando modifica i valori delle proprietà. Ad esempio, quando si esegue il data binding alle entità, l'impostazione di una proprietà può generare notifiche agli Stati Uniti, che non si verificano quando si imposta direttamente il campo. A tale scopo, è possibile modificare per PropertyAccessMode :

Le modalità di Field accesso alle proprietà e PreferField causeranno l'accesso al valore della proprietà tramite il relativo campo sottostante. Allo stesso modo, Property e PreferProperty causerà ef Core ad accedere al valore della proprietà tramite il relativo getter e setter.

Se Field o Property vengono usati e EF Core non può accedere al valore rispettivamente tramite il getter o la proprietà getter/setter della proprietà, EF Core genererà un'eccezione. In questo modo EF Core usa sempre l'accesso a campi/proprietà quando si ritiene che sia.

D'altra parte, le PreferField modalità e PreferProperty eseguiranno il fallback all'uso della proprietà o del campo sottostante rispettivamente se non è possibile usare l'accesso preferito. Il valore predefinito è PreferField. Ciò significa che EF Core userà i campi ogni volta che può, ma non avrà esito negativo se è necessario accedere a una proprietà tramite il relativo getter o setter.

FieldDuringConstruction e PreferFieldDuringConstruction configurare EF Core per l'uso dei campi di backup solo durante la creazione di 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 per la 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 di chiave temporanei durante il rilevamento di nuove entità con valori di chiave reali generati dal database quando viene chiamato SaveChanges. Per una panoramica dell'uso di questi valori temporanei, vedere Rilevamento modifiche in EF Core.

Accesso ai valori temporanei

I valori temporanei vengono archiviati nello strumento di rilevamento delle modifiche e non vengono impostati direttamente sulle istanze di entità. Tuttavia, questi valori temporanei vengono esposti quando si usano i vari meccanismi per l'accesso alle entità rilevate. 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à può essere creata in un client Web e quindi serializzata di nuovo nel server. I valori di 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à tramite chiave esterna, consentendo comunque la generazione di valori di 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 temporanei; questo non è obbligatorio, ma è una convenzione comune per evitare scontri chiave.
  • Alla Post.BlogId proprietà FK viene assegnato lo stesso valore negativo dell'infrastruttura a chiave pubblica del blog associato.
  • I valori PK vengono contrassegnati come temporanei impostando IsTemporary dopo che ogni entità viene rilevata. Ciò è necessario perché si presuppone che qualsiasi valore della chiave fornito dall'applicazione sia un valore di chiave reale.

Esaminando la visualizzazione di debug dello strumento di rilevamento delle modifiche prima di chiamare SaveChanges, i valori PK sono contrassegnati come temporanei e i post sono associati ai blog corretti, inclusa la correzione 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: []

Utilizzo dei valori predefiniti

EF Core consente a una proprietà di ottenere il valore predefinito dal database quando SaveChanges viene chiamato. Analogamente ai valori di chiave generati, EF Core userà un valore predefinito solo 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 consentirà 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);

Esaminando la visualizzazione debug di Rilevamento modifiche viene mostrato che il primo token è stato ValidFrom 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 di 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 nella 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 l'impostazione predefinita CLR per il tipo. Questo comportamento funziona correttamente nella maggior parte dei casi, ma significa che l'impostazione predefinita CLR non può essere inserita 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 valore predefinito del database -1:

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

L'intenzione è che l'impostazione predefinita di -1 verrà usata ogni volta che non viene impostato un valore esplicito. Tuttavia, l'impostazione del valore su 0 (impostazione predefinita CLR per i numeri interi) non è indistinguibile a EF Core dall'impostazione di 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 è ancora 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; }
}

In questo modo il valore predefinito CLR è Null, anziché 0, il che significa che 0 verrà ora inserito quando 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

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

La proprietà può essere lasciata non nullable, con solo il campo sottostante che può essere nullable. Ad esempio:

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

    private int? _count;

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

In questo modo è possibile inserire il valore predefinito CLR (0) se la proprietà è impostata in modo esplicito su 0, senza dover 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à booleane con valori predefiniti generati 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 oppure può essere lasciata non impostata, nel qual caso verrà utilizzata l'impostazione predefinita 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

In alcuni casi è utile avere impostazioni predefinite nello schema del database creato dalle migrazioni di EF Core senza EF Core che usano mai questi valori per gli inserimenti. A tale scopo, è possibile configurare la proprietà come PropertyBuilder.ValueGeneratedNever ad esempio:

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