EF Core Second Level Cache Interceptor

repository·master·Indexed 21 days ago

https://github.com/vahidn/efcoresecondlevelcacheinterceptor

A query caching layer for Entity Framework Core that stores EF command results in a cache provider to reduce database hits. It supports multiple providers including In-Memory, StackExchange.Redis, FusionCache, HybridCache, EasyCaching.Core, and CacheManager.Core. Features include the .Cacheable() extension for specific queries, global caching strategies by table or type, automatic invalidation on SaveChanges(), and advanced rules to skip caching or invalidation based on command text or results.

Tokens
5.8K
Snippets
16
Records
19
Agent score
26%

What's inside EFCoreSecondLevelCacheInterceptor

  1. Best practices for using EF Core Second Level Cache Interceptor

    master

    To use the interceptor effectively, follow these guidelines:

    • Selection Criteria: Cache global site settings, public data (e.g., articles, comments), or user-specific data that changes infrequently. Avoid caching small, frequently changing per-user data; use alternatives like user claims in cookies instead.
    • User Scoping: The cache is application-scoped, not user-scoped. It does not use session variables. To cache per-user data safely, you must include a user ID filter in your query, such as .Where(x => x.UserId == id).
    • Invalidation: The cache automatically updates when entities are inserted, updated, or deleted via a DbContext using this interceptor. Note that modifications made outside the interceptor (e.g., stored procedures, triggers, or other applications) will result in stale cache data.
    • Transactions: By default, queries inside an explicit transaction (context.Database.BeginTransaction()) are not cached. However, CRUD operations within that transaction will still trigger cache invalidation. To enable caching within transactions, use .AllowCachingWithExplicitTransactions(true).
    • Database Compatibility: If your database provider lacks support for types like DateTimeOffset or TimeSpan, you must configure EF Core [value converters] in your DbContext to handle these types.
  2. Configure Cache Locking (CacheLockOptions)

    master

    To prevent contention when multiple requests attempt to populate or read the same cache entry concurrently, you can configure cache locking using CacheLockOptions.

    Supported EFLockMode values:

    • None: No locking at all.
    • Global: A single global lock (default/backwards-compatible).
    • Keyed: A lock per computed cache key (recommended for single-process environments to reduce contention).

    Use CacheLockOptions(EFLockMode mode, TimeSpan duration) to configure this.

    services.AddEFSecondLevelCache(options =>
    {
        // reduce contention between different cache keys
        options.CacheLockOptions(EFLockMode.Keyed, TimeSpan.FromSeconds(15));
    });
  3. How Cache Invalidation Works

    master

    Automatic Invalidation

    The interceptor automatically invalidates cache entries when SaveChanges() or SaveChangesAsync() is called. It detects which tables were modified and clears dependent cached queries.

    Warning: Bulk operations like ExecuteUpdate and ExecuteDelete bypass EF Core interceptors and will not trigger automatic invalidation. You must invalidate these manually.

    Manual Invalidation

    Inject IEFCacheServiceProvider to manage the cache manually:

    • Clear everything: _cacheServiceProvider.ClearAllCachedEntries();
    • Invalidate specific tables: Use _cacheServiceProvider.InvalidateCacheDependencies(new EFCacheKey(tableNames)) where tableNames includes the prefixed table names (e.g., EF_TableName).

    Invalidation Notifications

    You can hook into invalidation events using .NotifyCacheInvalidation(action) during service registration to log or react to cache clears.

    // Manual invalidation example
    var tableNames = new HashSet<string> { "EF_TableName1", "EF_TableName2" };
    _cacheServiceProvider.InvalidateCacheDependencies(new EFCacheKey(tableNames));
  4. Skip cache invalidation for specific commands

    master

    If you have commands that update data but should not trigger a cache invalidation (for example, updating a view counter or a timestamp), use SkipCacheInvalidationCommands. This method accepts a predicate that evaluates the command text.

    services.AddEFSecondLevelCache(options =>
    {
        options.SkipCacheInvalidationCommands(commandText =>
            commandText.Contains("UPDATE [Posts] SET [Views]", StringComparison.InvariantCultureIgnoreCase));
    });
  5. Register the Cache Provider and Interceptor

    master

    You must register the second-level cache services in your Startup.cs or Program.cs and then add the SecondLevelCacheInterceptor to your DbContext registration using dependency injection.

    1. Register Services: Use AddEFSecondLevelCache to configure your provider (e.g., UseMemoryCacheProvider).

    2. Add Interceptor to DbContext: When configuring your DbContext (e.g., via AddDbContextPool), use .AddInterceptors() and retrieve the SecondLevelCacheInterceptor from the IServiceProvider.

    // 1. Register Services
    services.AddEFSecondLevelCache(options =>
        options.UseMemoryCacheProvider()
               .ConfigureLogging(true)
               .UseCacheKeyPrefix("EF_")
               .UseDbCallsIfCachingProviderIsDown(TimeSpan.FromMinutes(1))
    );
    
    // 2. Add Interceptor to DbContext
    services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
        optionsBuilder
            .UseSqlServer(connectionString)
            .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()));
  6. Verify Caching with Logging

    master

    To confirm the interceptor is working, enable logging in your configuration and set the log level to Debug in appsettings.json.

    1. Configuration:

    options.UseMemoryCacheProvider().ConfigureLogging(true);

    2. appsettings.json:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Debug",
          "Microsoft": "Debug"
        }
      }
    }

    Expected Log Output (Cache Hit):

    Suppressed result with a TableRows[...] from the cache[KeyHash: EB153BD4, CacheDependencies: Page.].
    Using the TableRows[...] from the cache.
  7. Install EF Core Second Level Cache Interceptor and Providers

    master

    To use the library, you must install the core package and at least one cache provider package via NuGet.

    Core Package:

    dotnet add package EFCoreSecondLevelCacheInterceptor

    Available Cache Providers:

    • In-Memory (Built-in): EFCoreSecondLevelCacheInterceptor.MemoryCache
    • StackExchange.Redis: EFCoreSecondLevelCacheInterceptor.StackExchange.Redis
    • FusionCache: EFCoreSecondLevelCacheInterceptor.FusionCache
    • HybridCache: EFCoreSecondLevelCacheInterceptor.HybridCache
    • EasyCaching.Core: EFCoreSecondLevelCacheInterceptor.EasyCaching.Core
    • CacheManager.Core: EFCoreSecondLevelCacheInterceptor.CacheManager.Core
    dotnet add package EFCoreSecondLevelCacheInterceptor
  8. Setup FusionCache provider

    master

    To use FusionCache, first register the FusionCache services with your desired options (like DefaultEntryOptions), then register the EF Core provider using UseFusionCacheProvider().

    // 1. Add FusionCache services with desired options
    services.AddFusionCache()
            .WithOptions(options =>
            {
                options.DefaultEntryOptions = new FusionCacheEntryOptions
                {
                    Duration = TimeSpan.FromMinutes(1),
                    IsFailSafeEnabled = true,
                    FailSafeMaxDuration = TimeSpan.FromHours(2),
                };
            });
    
    // 2. Add the EF Core Caching provider
    services.AddEFSecondLevelCache(options => options.UseFusionCacheProvider());
  9. Setup StackExchange.Redis provider

    master

    The EFCoreSecondLevelCacheInterceptor.StackExchange.Redis provider uses StackExchange.Redis and is preconfigured with a MessagePack serializer. Pass a ConfigurationOptions object to UseStackExchangeRedisCacheProvider along with a TimeSpan for the cache duration.

    var redisOptions = new ConfigurationOptions
    {
         EndPoints = { { "127.0.0.1", 6379 } },
         AllowAdmin = true,
         ConnectTimeout = 10000
    };
    
    services.AddEFSecondLevelCache(options =>
        options.UseStackExchangeRedisCacheProvider(redisOptions, TimeSpan.FromMinutes(5)));
  10. Configure advanced caching rules (Skip Caching)

    master

    You can define rules to skip caching for specific queries based on their SQL command text or the resulting data. This is useful for queries that are highly dynamic or return data that shouldn't be stored.

    • Skip by Command Text: Use SkipCachingCommands to provide a predicate that inspects the SQL command string.
    • Skip by Result: Use SkipCachingResults to provide a predicate that inspects the query result. You can check for null or use EFTableRows to check the RowsCount.
    • Using Query Tags: Instead of using the .NotCacheable() extension method, you can use EF Core's .TagWith("tag-name") method. This adds a comment to the SQL. You can then configure SkipCachingCommands to look for that specific comment string.
    // Skip by command text
    services.AddEFSecondLevelCache(options =>
    {
        options.SkipCachingCommands(commandText =>
            commandText.Contains("NEWID()", StringComparison.InvariantCultureIgnoreCase));
    });
    
    // Skip by result
    services.AddEFSecondLevelCache(options =>
    {
        options.SkipCachingResults(result =>
            result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0));
    });
    
    // Using Query Tags
    // 1. Tag the query in EF Core
    var blogs = await context.Blogs
        .TagWith("Fetching data")
        .Where(b => b.IsActive)
        .ToListAsync();
    
    // 2. Configure skip rule based on the tag
    services.AddEFSecondLevelCache(options =>
    {
        options.SkipCachingCommands(commandText =>
            commandText.Contains("-- Fetching data", StringComparison.InvariantCultureIgnoreCase));
    });
  11. Setup EasyCaching.Core provider

    master

    You can use EasyCaching.Core as a highly configurable cache manager. You can use it with In-Memory or Redis providers. For multi-tenancy, you can provide a delegate to UseEasyCachingCoreProvider that selects a provider name dynamically based on the current context (e.g., a tenant ID from IHttpContextAccessor).

    ```csharp
    // Example: In-Memory with EasyCaching.Core
    const string providerName = "InMemory1";
    services.AddEFSecondLevelCache(options =>
        options.UseEasyCachingCoreProvider(providerName, isHybridCache: false)
               .UseCacheKeyPrefix("EF_")
    );
    
    services.AddEasyCaching(options =>
    {
        options.UseInMemory(config =>
        {
            config.DBConfig = new InMemoryCachingOptions { SizeLimit = 10000 };
            config.MaxRdSecond = 120;
        }, providerName);
    });
    
    // Example: Dynamic Provider for Multi-tenancy
    services.AddEFSecondLevelCache(options =>
        options.UseEasyCachingCoreProvider(
           (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Headers["tenant-id"],
           isHybridCache: false)
    );