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 Add
e non AddAsync
.
Altri metodi simili, ad esempio Update
, Attach
e 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
, Attach
e 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 DetectChanges
automaticamente , 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 DetectChanges
automaticamente .
Metodi DbContext e DbSet
Molti metodi, tra cui Add
, Attach
Update
, 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
, Attach
Update
, 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 :
- Tutti i tipi di entità nel modello usando ModelBuilder.UsePropertyAccessMode
- Tutte le proprietà e gli spostamenti di un tipo di entità specifico usando EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Una proprietà specifica che usa PropertyBuilder.UsePropertyAccessMode
- Spostamento specifico tramite NavigationBuilder.UsePropertyAccessMode
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();