我认为这个问题的关键是异常究竟意味着什么:-
一个实体对象不能被多个 IEntityChangeTracker 实例引用。
我突然想到,这个异常可能是实体框架抱怨一个对象的实例已在多个 DbContexts 中更改,而不是简单地被多个 DbContexts 中的对象引用。我的理论基于这样一个事实,即生成的 POCO 类具有反向 FK 导航属性,并且实体框架自然会尝试修复这些反向导航属性,作为将实体图附加到 DbContext 的过程的一部分(参见a description of the fix-up process)
为了测试这个理论,我创建了一个简单的测试项目,我可以在其中启用和禁用反向导航属性。令我非常高兴的是,我发现这个理论是正确的,并且只要对象本身不改变,EF 很高兴对象被多次引用 - 这包括导航属性被改变 通过修复过程。
所以问题的答案只是遵循 2 条规则:-
- 确保 静态数据 对象永远不会更改(理想情况下,它们应该没有公共 setter 属性)和
- 不要包含任何指向引用类的 FK 反向导航属性。对于Reverse POCO Generator 的用户,我已向 Simon Hughes(作者)提出了一项建议,建议添加一项增强功能,使其成为配置选项。
我已包含以下测试类:-
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());
}
}