如何让EF每次DbContext查询重建视图缓存?测试中重载EF的方法?
针对你的测试场景和两个问题,我来分享下可行的解决方案:
你的测试场景与问题
我编写了测试以收集Entity Framework首次及后续查询的相关指标,测试代码如下:
[Test] [TestCase(1)] [TestCase(10)] public void FatEF_Init(int _id) { // Arrange. // Act. _watch.Reset(); _watch.Start(); //Create dbContext, work with them and then dispose it. using (var qmsUow = new FatDbContext(_sqlConnection.ToString())) { var result = qmsUow.SomeEntities.SingleOrDefault(se => se.Id == _id); _watch.Stop(); // Fill DTO. _info = new QueryDataDTO { Id = _id, DurationTime = _watch.ElapsedMilliseconds, }; // Assert. Debug.WriteLine($"id: {_id}. {_info.DurationTime} ms."); } }如您所见,我使用不同参数运行测试两次。首次调用(“冷启动”)时,Entity Framework会完成初始化过程并将自身缓存到应用域中(据我理解),第二次查询则处于“热启动”状态,速度快得多。测试结果显示首次执行耗时约9秒,第二次约2毫秒,但我想了解10次首次“冷启动”请求的平均耗时。现提出两个问题:
- 如何在测试迭代之间从内存中卸载并重新加载Entity Framework(或清除视图缓存)?
- 是否可以在每次查询时重建视图缓存?
问题1:测试迭代间卸载/重新加载EF或清除视图缓存
EF的元数据、视图缓存本质是绑定在**应用域(AppDomain)**或内部静态缓存中的,要实现每次测试都触发冷启动,最彻底的方式是让每个测试运行在独立的应用域中。这里以NUnit为例给出实现思路:
自定义应用域隔离测试逻辑
你可以把测试核心逻辑封装到一个继承MarshalByRefObject的类中,每次测试创建新的AppDomain执行逻辑,结束后卸载该域,就能彻底清空EF的所有缓存:[Test] [TestCase(1)] [TestCase(10)] [NonParallelizable] public void FatEF_Init(int _id) { // 创建独立测试应用域 var testDomain = AppDomain.CreateDomain($"EFTestDomain_{_id}"); try { // 跨域创建测试逻辑实例 var tester = (EFTestExecutor)testDomain.CreateInstanceAndUnwrap( typeof(EFTestExecutor).Assembly.FullName, typeof(EFTestExecutor).FullName); // 执行冷启动测试 tester.RunColdStartTest(_sqlConnection.ToString(), _id); } finally { // 卸载应用域,清空所有缓存 AppDomain.Unload(testDomain); } } // 跨域执行的测试逻辑类 public class EFTestExecutor : MarshalByRefObject { public void RunColdStartTest(string connectionString, int id) { var watch = Stopwatch.StartNew(); using (var dbContext = new FatDbContext(connectionString)) { var result = dbContext.SomeEntities.SingleOrDefault(se => se.Id == id); watch.Stop(); Debug.WriteLine($"id: {id}. 冷启动耗时: {watch.ElapsedMilliseconds} ms."); } } }这种方式能确保每次测试都是完全独立的冷启动状态,适合统计多次冷启动的平均耗时。
EF Core的轻量化替代方案
如果你使用的是EF Core,也可以通过自定义IModelCacheKeyFactory让每个DbContext实例使用唯一的模型缓存键,避免复用缓存。不过这种方式不如AppDomain隔离彻底,适合对性能要求稍高的场景:public class UniqueModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { // 每次生成唯一键,强制重建模型缓存 return Guid.NewGuid(); } } // 创建DbContext时替换默认缓存键工厂 var options = new DbContextOptionsBuilder<FatDbContext>() .UseSqlServer(_sqlConnection.ToString()) .ReplaceService<IModelCacheKeyFactory, UniqueModelCacheKeyFactory>() .Options; using (var dbContext = new FatDbContext(options)) { // 执行查询,此时会重建模型缓存 }
问题2:每次查询时重建视图缓存
EF默认会缓存视图以提升性能,但如果需要强制每次查询都重建,可以根据EF版本选择不同方式:
EF6中通过反射清除缓存
EF6的MetadataWorkspace有内部的ClearCache方法,通过反射调用可以清空视图缓存,之后执行查询就会重建:using (var dbContext = new FatDbContext(connectionString)) { var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; var metadataWorkspace = objectContext.MetadataWorkspace; // 反射调用内部方法清空缓存 var clearCacheMethod = metadataWorkspace.GetType() .GetMethod("ClearCache", BindingFlags.Instance | BindingFlags.NonPublic); clearCacheMethod?.Invoke(metadataWorkspace, null); // 此时执行查询会重新生成视图 var result = dbContext.SomeEntities.SingleOrDefault(se => se.Id == id); }注意:内部方法可能随EF版本变更,需要做好兼容性测试。
EF Core中强制重建视图
结合前面提到的UniqueModelCacheKeyFactory,每次创建DbContext都会重建模型和视图,自然实现每次查询都触发视图重建。或者你也可以手动调用dbContext.Model.GetEntityTypes()触发模型初始化,不过前者更彻底。
内容的提问来源于stack exchange,提问作者AllmanTool

