Entity Framework Core中导航属性的缓存方案咨询
EF Core 缓存极少变动的Customer导航属性方案
针对你提出的场景——Customer数据量少、极少变动,希望在加载Order的Customer导航属性时用内存缓存替代关联查询,有几个实用方案可选:
方案1:预加载Customer到内存+EF查询拦截器(保留Include写法)
这个方案可以让你继续用原来的Include查询,同时自动用缓存数据替代数据库查询,对现有代码侵入最小。
- 预加载Customer到内存缓存
在应用启动时(比如Program.cs初始化阶段),一次性把所有Customer加载到内存字典:
// 假设在Program.cs中 var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>(); var allCustomers = await dbContext.Customers.AsNoTracking().ToListAsync(); // 用字典存,方便快速查找 var customerCache = new Dictionary<int, Customer>(allCustomers.ToDictionary(c => c.Id)); // 如果需要更灵活的缓存(比如支持过期),可以用IMemoryCache var memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>(); memoryCache.Set("AllCustomers", customerCache, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromDays(7), // 7天未访问自动过期 AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30) // 强制30天更新一次 });
- 编写查询拦截器
创建一个EF查询拦截器,拦截所有针对Customer的查询,直接返回缓存数据:
public class CustomerCacheInterceptor : IQueryInterceptor { private readonly Dictionary<int, Customer> _customerCache; public CustomerCacheInterceptor(Dictionary<int, Customer> customerCache) { _customerCache = customerCache; } public ValueTask<QueryExpressionResult> QueryCompilationStartingAsync( QueryExpressionStartingEventData eventData, CancellationToken cancellationToken = default) { // 判断当前查询是否是针对Customer实体 if (eventData.QueryExpression is EntityQueryRootExpression root && root.EntityType.ClrType == typeof(Customer)) { // 把查询替换为内存中缓存的Customer集合 var cachedQuery = _customerCache.Values.AsQueryable().Expression; return ValueTask.FromResult(QueryExpressionResult.Create(cachedQuery)); } // 非Customer查询,不做修改 return ValueTask.FromResult(QueryExpressionResult.NoChanges); } }
- 注册拦截器到DbContext
在DbContext配置或服务注册时添加这个拦截器:
// 方式1:在DbContext的OnConfiguring方法中 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.AddInterceptors(new CustomerCacheInterceptor(customerCache)); } // 方式2:在Program.cs注册服务时 builder.Services.AddDbContext<YourDbContext>(options => { options.UseSqlServer(yourConnectionString); options.AddInterceptors(new CustomerCacheInterceptor(customerCache)); });
配置完成后,你原来的dbContext.Order.Include(c => c.Customer).First()会自动使用缓存的Customer数据,不会再触发关联查询。
方案2:手动关联缓存数据(最简实现)
如果不想用拦截器,直接手动处理导航属性也很简单,缺点是需要修改查询代码:
// 先查询Order,不Include导航属性 var order = await dbContext.Order.FirstAsync(); // 从缓存字典中获取对应的Customer order.Customer = customerCache[order.CustomerId]; // 假设Order有CustomerId外键字段
这种方式代码量最少,适合小型项目或对代码侵入度要求低的场景。
方案3:第三方二级缓存库(开箱即用)
用成熟的第三方二级缓存库,比如EFCoreSecondLevelCacheInterceptor,可以快速实现实体级别的缓存:
- 安装NuGet包
Install-Package EFCoreSecondLevelCacheInterceptor
- 注册服务和拦截器
builder.Services.AddEFSecondLevelCache(); builder.Services.AddDbContext<YourDbContext>(options => { options.UseSqlServer(yourConnectionString); options.AddInterceptors(new SecondLevelCacheInterceptor()); });
- 配置Customer实体的缓存规则
在DbContext的OnModelCreating方法中,给Customer实体配置长期缓存:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置Customer实体缓存,30天过期 modelBuilder.Entity<Customer>() .Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromDays(30)); }
之后查询Order并Include Customer时,EF会自动从二级缓存中读取Customer数据,无需额外代码。如果Customer需要更新,记得调用缓存清理方法:
await dbContext.CacheManager.ClearCacheAsync(typeof(Customer));
注意事项
- 因为Customer极少变动,缓存过期时间可以设得长一些,比如几周;如果有更新操作,一定要同步清理缓存,避免数据不一致。
- 方案1的拦截器方式完全保留原有查询写法,适合需要兼容现有代码的场景;方案2最简,适合快速实现;方案3依赖第三方库,配置简单但需要熟悉库的用法。
内容的提问来源于stack exchange,提问作者Alois
相关产品推荐
相关产品推荐

