Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/321.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 实体框架-如何缓存和共享只读对象_C#_Entity Framework_Caching_Entity Framework 6_Dbcontext - Fatal编程技术网

C# 实体框架-如何缓存和共享只读对象

C# 实体框架-如何缓存和共享只读对象,c#,entity-framework,caching,entity-framework-6,dbcontext,C#,Entity Framework,Caching,Entity Framework 6,Dbcontext,我们有一个具有相当复杂实体模型的应用程序,其中高性能和低延迟至关重要,但我们不需要横向可伸缩性。除了自托管的ASP.NET Web API 2之外,该应用程序还有许多事件源。我们使用EntityFramework6将POCO类映射到数据库(我们使用优秀的生成类) 每当事件到达时,应用程序必须对实体模型进行一些调整,并通过EF将此增量调整持久化到数据库中。同时,读取或更新请求可能通过Web API到达 由于模型涉及许多表和FK关系,并且对事件作出反应通常需要加载主题实体下的所有关系,因此我们选择在

我们有一个具有相当复杂实体模型的应用程序,其中高性能和低延迟至关重要,但我们不需要横向可伸缩性。除了自托管的ASP.NET Web API 2之外,该应用程序还有许多事件源。我们使用EntityFramework6将POCO类映射到数据库(我们使用优秀的生成类)

每当事件到达时,应用程序必须对实体模型进行一些调整,并通过EF将此增量调整持久化到数据库中。同时,读取或更新请求可能通过Web API到达

由于模型涉及许多表和FK关系,并且对事件作出反应通常需要加载主题实体下的所有关系,因此我们选择在内存缓存中维护整个数据集,而不是为每个事件加载整个对象图。下图显示了我们模型的简化版本:-

在程序启动时,我们通过一个临时的
DbContext
加载所有感兴趣的
ClassA
实例(及其相关的依赖关系图),并将其插入字典(即缓存)。当事件到达时,我们在缓存中找到ClassA实例,并通过
DbSet.attach()
将其附加到每个事件
DbContext
。整个程序都是使用wait异步模式编写的,可以同时处理多个事件。我们通过使用锁来保护缓存对象不被并发访问,因此我们保证缓存的
ClassA
只能一次加载到
DbContext
中。到目前为止,性能非常好,我们对该机制感到满意但是有一个问题。虽然在< ClassA > <代码>下,实体图是相当独立的,但是有一些PoCO类表示我们认为是只读的静态数据(在图像中的橙色阴影)。我们发现EF有时会抱怨

一个实体对象不能被多个IEntityChangeTracker实例引用

当我们试图同时
Attach()
两个不同的
ClassA
实例时(即使我们附加到不同的
Dbcontexts
),因为它们共享对同一
ClassAType
的引用。下面的代码片段演示了这一点:-

    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
concurrentCache=null;
使用(var ctx=new MyDbContext())
{
var classAs=ctx.classAs
.Include(a=>a.ClassAType)
.ToList();
theCache=新的ConcurrentDictionary(classAs.ToDictionary(a=>a.ID));
}
//取引用同一ClassA类型的ClassA的两个不同实例
//并将它们加载到单独的DBContext中
var ctx1=新的MyDbContext();
ctx1.ClassAs.Attach(缓存[1]);
var ctx2=新的MyDbContext();
ctx2.ClassAs.Attach(缓存[2]);//这里抛出异常

有没有办法通知EF
ClassAType
是只读/静态的,我们不希望它确保每个实例只能加载到一个
DbContext
?到目前为止,我发现解决这个问题的唯一方法是修改POCO生成器以忽略这些FK关系,因此它们不是实体模型的一部分。但这会使编程复杂化,因为
ClassA
中有一些处理方法需要访问静态数据。

我认为这可能会起作用:在程序启动时选择这些实体数据库集时,尝试在这些实体数据库集中使用
AsNoTracking

dbContext.ClassEType.AsNoTracking();
这将禁用对它们的更改跟踪,因此EF不会尝试持久化它们


此外,这些实体的POCO类应该只有只读属性(没有
set
方法)。

我认为这个问题的关键是异常的确切含义:-

一个实体对象不能被多个IEntityChangeTracker实例引用

我突然想到,这个例外可能是实体框架抱怨一个对象的实例在多个
DbContexts
中被更改,而不是简单地被多个
DbContexts
中的对象引用。我的理论基于这样一个事实:生成的POCO类具有反向FK导航属性,实体框架自然会尝试修复这些反向导航属性,作为将实体图附加到
DbContext
过程的一部分(请参阅)

为了验证这个理论,我创建了一个简单的测试项目,可以在其中启用和禁用反向导航属性。令我非常高兴的是,我发现这个理论是正确的,而且只要对象本身不发生变化,EF就非常乐意多次引用对象,这包括通过修复过程改变导航属性

因此,这个问题的答案只是遵循两条规则:

  • 确保静态数据对象从未更改(理想情况下,它们不应具有公共setter属性),并且
  • 不要包含任何指向引用类的FK反向导航属性。对于的用户,我建议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));
    }
}
类程序
{
静态void Main(字符串[]参数)
{
ConcurrentDictionary theCache=null;
尝试
{
使用(var ctx=new MyDbContext())
{
var classAs=ctx.classAs
.Include(a=>a.ClassAType)
.ToList();
theCache=新的ConcurrentDictionary(classAs.ToDictionary(a=>a.ID));
}
//以引用同一ClassA类型的2个ClassA实例为例
//并将它们加载到单独的DBContext中
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());
    }
}