Search code examples
c#asp.net-coreasp.net-core-3.1distributed-cachingresponsecache

Using ASP.NET Core 3.1 custom distributed cache for respones caching


I wrote a custom distributed cache, based on Azure BlobStorage, to optimized page speed. The website should deliver the cached page from cache, till cached page has been expired. This implementation should work like the existing DistributedInMemoryCache, DistributedRedisCache ot NCacheDistributedCache. The howto is descript here https://learn.microsoft.com/de-de/aspnet/core/performance/caching/distributed?view=aspnetcore-5.0

My problem is, the methods to get or set in my cache aren't executed.

I implemented the IDistributedCache in as DistributedBlobStorageCache and registered it with helping of a ServiceCollection extension AddDistributedBlobStorageCache(). So fare so good.

The action has the ResponseCacheAttribute above and the cache profile is setup in Startup.cs. From my understanding, is the system correct configured, but the Get/GetAsync or Set/SetAsync method of distributed cache are executed.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();
        services.AddAntiforgery();
        services.AddResponseCaching();
        services.AddDistributedBlobStorageCache(options =>
        {
            options.ConnectionString = "<my connection string>";
        });
        services.AddResponseCompression();
        services.AddHttpsRedirection(options => options.RedirectStatusCode = 301);
        services.AddControllersWithViews(
            options =>
            {
                options.RespectBrowserAcceptHeader = true;

                options.CacheProfiles.Add(new KeyValuePair<string, CacheProfile>("test", new CacheProfile
                {
                    Duration = 60
                }));

                // authorization filters
                options.Filters.Add<AutoValidateAntiforgeryTokenAttribute>();
            });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseCors();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseResponseCaching();
        app.UseResponseCompression();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

public static class BlobStorageCacheServiceCollectionExtensions
{
    public static IServiceCollection AddDistributedBlobStorageCache(this IServiceCollection services, Action<BlobStorageCacheOptions> options)
    {
        if (options != default)
        {
            services.AddOptions();
            services.Configure(options);
        }
        return services.AddSingleton<IDistributedCache, DistributedBlobStorageCache>();
    }
}

public class BlobStorageCacheOptions : DistributedCacheEntryOptions
{
    public string ConnectionString { get; set; }
}

public class DistributedBlobStorageCache : IDistributedCache
{
    private readonly ILoggerFactory _loggerFactory;
    private readonly BlobStorageCacheOptions _options;

    public DistributedBlobStorageCache(ILoggerFactory loggerFactory, IOptions<BlobStorageCacheOptions> optionsAccessor)
    {
        _loggerFactory = loggerFactory;
        _options = optionsAccessor?.Value;
    }

    public byte[] Get(string key)
    {
        return GetAsync(key).GetAwaiter().GetResult();
    }

    public async Task<byte[]> GetAsync(string key, CancellationToken token = new CancellationToken())
    {
        var repos = CreateRepository();
        var cacheItem = await repos.GetAsync(key, token);
        if (cacheItem == null || cacheItem.ContentBytes == null)
            return Array.Empty<byte>();
        return cacheItem.ContentBytes;
    }

    public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
    {
        SetAsync(key, value, options).GetAwaiter().GetResult();
    }

    public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options,
        CancellationToken token = new CancellationToken())
    {
        var cacheItem = new CacheItem
        {
            ContentBytes = value,
            Key = key,
            UtcExpiry = options.AbsoluteExpiration.GetValueOrDefault(DateTimeOffset.UtcNow).DateTime
        };
        var repos = CreateRepository();
        await repos.SaveAsync(cacheItem, token);
    }

    public void Refresh(string key)
    {
        // not needed, because we use no sliding expiration
    }

    public Task RefreshAsync(string key, CancellationToken token = new CancellationToken())
    {
        // not needed, because we use no sliding expiration
        return Task.CompletedTask;
    }

    public void Remove(string key)
    {
        RemoveAsync(key).GetAwaiter().GetResult();
    }

    public async Task RemoveAsync(string key, CancellationToken token = new CancellationToken())
    {
        var repos = CreateRepository();
        await repos.RemoveAsync(key, token);
    }

    private BlobStorageCacheRepository CreateRepository()
    {
        return new BlobStorageCacheRepository(_options.ConnectionString);
    }

    private class BlobStorageCacheRepository
    {
        public BlobStorageCacheRepository(string connectionString)
        {
            
        }

        internal Task<CacheItem> GetAsync(string key, CancellationToken token)
        {
            // to implement
            return Task.FromResult(new CacheItem());
        }

        internal Task SaveAsync(CacheItem item, CancellationToken token)
        {
            // to implement
            return Task.CompletedTask;
        }

        internal Task RemoveAsync(string key, CancellationToken token)
        {
            // to implement
            return Task.CompletedTask;
        }
    }

    private class CacheItem
    {
        internal byte[] ContentBytes { get; set; }

        internal string Key { get; set; }

        internal DateTimeOffset UtcExpiry { get; set; }
    }
}   

Solution

  • Actually, what you're doing are two separate things

    About response caching

    services.AddResponseCaching() and app.UseResponseCaching() are use to register ObjectPoolProvider and ResponseCachingMiddleware correspondingly, and this middleware have responsible for caching of our Action endpoint, which ever have [ResponseCache] above it.

    This middleware have no dependency to IDistributedCache which we mention above. Therefore, two non-relative thing, they are not have anything related in anyway.

    About IDistributedCache

    This was design as a barebone for DistributedCache as .net core team saw it well-fit for most of the case. IDistributedCache could be implemented by many mechanism, including RMDB, DocumentDb, Redis,... and even in this case, blob storage. But, remember, the barebone doen't know anything about the implementation detail (create an entry option that have sliding expired doing nothing if we are not implement that mechanism).

    For example: MemoryCache instance, which implement IMemoryCache (can be found at Microsoft.Extensions.Caching.Memory), have extension Set<TItem> that handle the process of storing the data and set option entry for that (like expiration time) as we can see here.

    So, even I saw your implementation on IDistributedCache, but i saw nothing about handle the expiration stuff, so... that's not gonna work... even if we try using it directly.

    There's no magic, that's just how the code got implemented.