使用 ASP.NET Core 3.1 自定义分布式缓存进行响应缓存

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

我编写了一个基于 Azure BlobStorage 的自定义分布式缓存,以优化页面速度。网站应该从缓存中传递缓存页面,直到缓存页面过期。此实现应该像现有的 DistributedInMemoryCache、DistributedRedisCache 或 NCacheDistributedCache 一样工作。 howto 在这里描述 https://docs.microsoft.com/de-de/aspnet/core/performance/caching/distributed?view=aspnetcore-5.0

我的问题是,在我的缓存中获取或设置的方法没有被执行。

我将 IDistributedCache 实现为 DistributedBlobStorageCache,并在 ServiceCollection 扩展的帮助下注册了它 AddDistributedBlobStorageCache()。太好了。

该操作具有上面的 ResponseCacheAttribute,缓存配置文件在 Startup.cs 中设置。据我了解,系统配置是否正确,但是执行了分布式缓存的Get/GetAsync或Set/SetAsync方法。

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; }
    }
}   

其实你做的是两件不同的事情

关于响应缓存

services.AddResponseCaching()app.UseResponseCaching()分别用来注册ObjectPoolProviderResponseCachingMiddleware,这个中间件负责缓存我们的Action端点,有[ResponseCache] 在上面。

这个中间件不依赖于我们上面提到的IDistributedCache。因此,两个不相关的东西,他们无论如何都没有任何关系。

关于IDistributedCache

这是为 DistributedCache 设计的准系统,因为 .net 核心团队认为它非常适合大多数情况。 IDistributedCache 可以通过多种机制实现,包括 RMDB、DocumentDb、Redis 等,甚至在这种情况下,还有 blob 存储。但是,请记住,准系统对实现细节一无所知(如果我们不实现该机制,则创建一个滑动过期的入口选项什么也不做)。

例如: MemoryCache 实例,它实现了 IMemoryCache(可以在 Microsoft.Extensions.Caching.Memory 找到),具有扩展 Set<TItem> 处理存储数据的过程并为此设置选项条目(如过期时间)正如我们所见here.

所以,即使我在 IDistributedCache 上看到了你的实现,但我没有看到任何关于处理过期的东西,所以......那是行不通的......即使我们尝试直接使用它。

没有魔法,代码就是这样实现的。