Abbiamo un'applicazione con un modello di entità abbastanza complesso in cui sono essenziali alte prestazioni e bassa latenza, ma non abbiamo bisogno di scalabilità orizzontale. L'applicazione ha un numero di sorgenti di eventi in aggiunta a un'API Web ASP.NET self-hosted. 2. Utilizziamo Entity Framework 6 per mappare dalle classi POCO al database (usiamo l'eccellente generatore inverso POCO per generare le nostre classi).
Ogni volta che arriva un evento, l'applicazione deve apportare alcune modifiche al modello dell'entità e mantenere questo aggiustamento delta nel database tramite EF. Allo stesso tempo, le richieste di lettura o aggiornamento possono arrivare tramite l'API Web.
Poiché il modello coinvolge molte tabelle e relazioni FK e la reazione a un evento richiede di solito tutte le relazioni sotto l'entità soggetto da caricare, abbiamo scelto di mantenere l'intero set di dati in una cache in memoria anziché caricare l'intero grafico dell'oggetto per ogni evento. L'immagine sotto mostra una versione semplificata del nostro modello: -
All'avvio del programma vengono caricate tutte le istanze di ClassA
interessanti (e il relativo grafico di dipendenza associato) tramite un DbContext
temporaneo e inserite in un dizionario (ad es. La nostra cache). Quando arriva un evento, troviamo l'istanza ClassA nella nostra cache e la DbContext
a un DbContext
per evento tramite DbSet.Attach()
. Il programma viene scritto utilizzando lo schema await-async e più eventi possono essere elaborati contemporaneamente. Proteggiamo gli oggetti memorizzati nella cache dall'accesso simultaneo mediante l'uso di blocchi, pertanto garantiamo che una ClassA
cache può essere caricata in un DbContext
solo una alla volta. Fin qui tutto bene, le prestazioni sono eccellenti e siamo contenti del meccanismo. Ma c'è un problema . Sebbene il grafico delle entità sia abbastanza autonomo in ClassA
, esistono alcune classi POCO che rappresentano ciò che consideriamo essere dati statici di sola lettura (ombreggiati in arancione nell'immagine). Abbiamo scoperto che EF a volte si lamenta
Un oggetto entità non può essere referenziato da più istanze di IEntityChangeTracker.
quando proviamo a collegare Attach()
due diverse istanze di ClassA
allo stesso tempo (anche se stiamo collegando a diversi Dbcontexts
) perché condividono un riferimento allo stesso ClassAType
. Questo è dimostrato dal frammento di codice qui sotto: -
ConcurrentDictionary<int,ClassA> theCache = null;
using(var ctx = new MyDbContext())
{
var classAs = ctx.ClassAs
.Include(a => a.ClassAType)
.ToList();
theCache = new ConcurrentDictionary<int,ClassA>(classAs.ToDictionary(a => a.ID));
}
// take 2 different instances of ClassA that refer to the same ClassAType
// and load them into separate DbContexts
var ctx1 = new MyDbContext();
ctx1.ClassAs.Attach(theCache[1]);
var ctx2 = new MyDbContext();
ctx2.ClassAs.Attach(theCache[2]); // exception thrown here
C'è un modo per informare EF che ClassAType
è di sola lettura / statico e non vogliamo che assicuri che ogni istanza possa essere caricata in un solo DbContext
? Finora l'unico modo per aggirare il problema che ho riscontrato è di modificare il generatore POCO per ignorare queste relazioni FK, quindi non fanno parte del modello di entità. Ma questo complica la programmazione perché in ClassA
ci sono metodi di elaborazione che richiedono l'accesso ai dati statici.
Penso che la chiave di questa domanda sia esattamente l'eccezione:
Un oggetto entità non può essere referenziato da più istanze di IEntityChangeTracker.
Mi è venuto in mente che forse questa eccezione è Entity Framework che lamenta che un'istanza di un oggetto è stata modificata in più DbContexts
anziché essere semplicemente referenziata da oggetti in più DbContexts
. La mia teoria era basata sul fatto che le classi POCO generate hanno proprietà di navigazione FK inverse e che Entity Framework tenterebbe naturalmente di correggere queste proprietà di navigazione inversa come parte del processo di collegamento del grafo delle entità a DbContext
(vedere una descrizione del processo di fix-up )
Per testare questa teoria ho creato un semplice progetto di test in cui ho potuto abilitare e disabilitare le proprietà di navigazione inversa. Con mia grande gioia ho scoperto che la teoria era corretta e che EF è abbastanza contento che gli oggetti vengano referenziati più volte fintanto che gli oggetti stessi non cambiano - e questo include le proprietà di navigazione modificate dal processo di correzione .
Quindi la risposta alla domanda è semplicemente seguire 2 regole: -
Ho incluso le classi di test qui sotto:
class Program
{
static void Main(string[] args)
{
ConcurrentDictionary<int,ClassA> theCache = null;
try
{
using(var ctx = new MyDbContext())
{
var classAs = ctx.ClassAs
.Include(a => a.ClassAType)
.ToList();
theCache = new ConcurrentDictionary<int,ClassA>(classAs.ToDictionary(a => a.ID));
}
// take 2 instances of ClassA that refer to the same ClassAType
// and load them into separate DbContexts
var classA1 = theCache[1];
var classA2 = theCache[2];
var ctx1 = new MyDbContext();
ctx1.ClassAs.Attach(classA1);
var ctx2 = new MyDbContext();
ctx2.ClassAs.Attach(classA2);
// When ClassAType has a reverse FK navigation property to
// ClassA we will not reach this line!
WriteDetails(classA1);
WriteDetails(classA2);
classA1.Name = "Updated";
classA2.Name = "Updated";
WriteDetails(classA1);
WriteDetails(classA2);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
System.Console.WriteLine("End of test");
}
static void WriteDetails(ClassA classA)
{
Console.WriteLine(String.Format("ID={0} Name={1} TypeName={2}",
classA.ID, classA.Name, classA.ClassAType.Name));
}
}
public class ClassA
{
public int ID { get; set; }
public string ClassATypeCode { get; set; }
public string Name { get; set; }
//Navigation properties
public virtual ClassAType ClassAType { get; set; }
}
public class ClassAConfiguration : System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<ClassA>
{
public ClassAConfiguration()
: this("dbo")
{
}
public ClassAConfiguration(string schema)
{
ToTable("TEST_ClassA", schema);
HasKey(x => x.ID);
Property(x => x.ID).HasColumnName(@"ID").IsRequired().HasColumnType("int").HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity);
Property(x => x.Name).HasColumnName(@"Name").IsRequired().HasColumnType("varchar").HasMaxLength(50);
Property(x => x.ClassATypeCode).HasColumnName(@"ClassATypeCode").IsRequired().HasColumnType("varchar").HasMaxLength(50);
//HasRequired(a => a.ClassAType).WithMany(b => b.ClassAs).HasForeignKey(c => c.ClassATypeCode);
HasRequired(a => a.ClassAType).WithMany().HasForeignKey(b=>b.ClassATypeCode);
}
}
public class ClassAType
{
public string Code { get; private set; }
public string Name { get; private set; }
public int Flags { get; private set; }
// Reverse navigation
//public virtual System.Collections.Generic.ICollection<ClassA> ClassAs { get; set; }
}
public class ClassATypeConfiguration : System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<ClassAType>
{
public ClassATypeConfiguration()
: this("dbo")
{
}
public ClassATypeConfiguration(string schema)
{
ToTable("TEST_ClassAType", schema);
HasKey(x => x.Code);
Property(x => x.Code).HasColumnName(@"Code").IsRequired().HasColumnType("varchar").HasMaxLength(12);
Property(x => x.Name).HasColumnName(@"Name").IsRequired().HasColumnType("varchar").HasMaxLength(50);
Property(x => x.Flags).HasColumnName(@"Flags").IsRequired().HasColumnType("int");
}
}
public class MyDbContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<ClassA> ClassAs { get; set; }
public System.Data.Entity.DbSet<ClassAType> ClassATypes { get; set; }
static MyDbContext()
{
System.Data.Entity.Database.SetInitializer<MyDbContext>(null);
}
const string connectionString = @"Server=TESTDB; Database=TEST; Integrated Security=True;";
public MyDbContext()
: base(connectionString)
{
}
protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new ClassAConfiguration());
modelBuilder.Configurations.Add(new ClassATypeConfiguration());
}
}
Penso che questo potrebbe funzionare: prova ad utilizzare AsNoTracking
in quelle entità DbSets quando li selezioni all'avvio del programma:
dbContext.ClassEType.AsNoTracking();
Questo disabiliterà il rilevamento delle modifiche per loro, quindi EF non tenterà di persisterli.
Inoltre, la classe POCO per quelle entità dovrebbe avere solo proprietà di sola lettura (senza alcun metodo set
).