You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core MVC中自定义缓存压缩响应的实现疑问

ASP.NET Core MVC自定义缓存与压缩实现方案

要实现你描述的自定义缓存+压缩逻辑,不建议直接依赖Output Caching,因为它的扩展灵活性有限,无法完全匹配你需要的自定义缓存键生成、压缩后缓存的全流程。更好的方式是通过自定义中间件结合自研缓存服务来实现,完全掌控每一步逻辑。

核心实现步骤

1. 定义请求标识与缓存服务

首先创建用于标识请求唯一性的POCO,以及自研缓存的接口和实现:

// 标识请求唯一性的POCO,可根据需求添加字段
public class RequestSignature
{
    public string Path { get; set; }
    public string QueryString { get; set; }
    public string UserId { get; set; } // 按用户隔离缓存时使用
}

// 自研缓存服务接口
public interface ICustomResponseCache
{
    Task<(byte[] ResponseBytes, Dictionary<string, string> Headers)> GetCachedResponseAsync(string cacheKey, string encodingType);
    Task CacheResponseAsync(string cacheKey, string encodingType, byte[] responseBytes, Dictionary<string, string> headers);
}

// 内存版自研缓存实现(替换为你的存储方案)
public class InMemoryCustomResponseCache : ICustomResponseCache
{
    private readonly ConcurrentDictionary<string, (byte[] ResponseBytes, Dictionary<string, string> Headers)> _cacheStore = new();

    public async Task<(byte[] ResponseBytes, Dictionary<string, string> Headers)> GetCachedResponseAsync(string cacheKey, string encodingType)
    {
        var fullKey = $"{cacheKey}_{encodingType}";
        return _cacheStore.TryGetValue(fullKey, out var entry) ? entry : (null, null);
    }

    public async Task CacheResponseAsync(string cacheKey, string encodingType, byte[] responseBytes, Dictionary<string, string> headers)
    {
        var fullKey = $"{cacheKey}_{encodingType}";
        _cacheStore[fullKey] = (responseBytes, headers);
    }
}

2. 实现自定义缓存压缩中间件

中间件将在授权完成后拦截请求,检查缓存;若未命中则继续执行MVC管道,最终拦截响应并缓存压缩后的结果:

public class CustomCacheCompressionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ICustomResponseCache _cache;

    public CustomCacheCompressionMiddleware(RequestDelegate next, ICustomResponseCache cache)
    {
        _next = next;
        _cache = cache;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 仅处理GET请求,可按需调整
        if (!HttpMethods.IsGet(context.Request.Method))
        {
            await _next(context);
            return;
        }

        // 构建请求标识POCO
        var requestSignature = new RequestSignature
        {
            Path = context.Request.Path.Value,
            QueryString = context.Request.QueryString.Value,
            UserId = context.User.Identity.IsAuthenticated 
                ? context.User.FindFirstValue(ClaimTypes.NameIdentifier) 
                : "anonymous"
        };

        // 生成自定义缓存键(基于请求标识哈希)
        var signatureJson = JsonSerializer.Serialize(requestSignature);
        var cacheKey = GenerateHash(signatureJson);

        // 获取客户端支持的压缩格式
        var acceptEncoding = context.Request.Headers.AcceptEncoding.ToString();
        var preferredEncoding = GetPreferredCompression(acceptEncoding);

        // 检查缓存是否命中
        var (cachedBytes, cachedHeaders) = await _cache.GetCachedResponseAsync(cacheKey, preferredEncoding);
        if (cachedBytes != null)
        {
            // 处理ETag匹配,返回304
            var clientEtag = context.Request.Headers.IFNoneMatch.FirstOrDefault();
            if (!string.IsNullOrEmpty(clientEtag) && cachedHeaders.TryGetValue("ETag", out var cachedEtag) && clientEtag == cachedEtag)
            {
                context.Response.StatusCode = StatusCodes.Status304NotModified;
                return;
            }

            // 写入缓存的响应头和内容
            foreach (var header in cachedHeaders)
            {
                context.Response.Headers.TryAdd(header.Key, header.Value);
            }
            context.Response.ContentLength = cachedBytes.Length;
            await context.Response.Body.WriteAsync(cachedBytes);
            return;
        }

        // 缓存未命中,拦截响应流
        var originalBody = context.Response.Body;
        using var responseStream = new MemoryStream();
        context.Response.Body = responseStream;

        try
        {
            // 执行MVC管道
            await _next(context);

            // 非成功响应不缓存
            if (!context.Response.StatusCode.ToString().StartsWith("2"))
            {
                responseStream.Seek(0, SeekOrigin.Begin);
                await responseStream.CopyToAsync(originalBody);
                return;
            }

            // 读取控制器返回的内容(已序列化为JSON)
            responseStream.Seek(0, SeekOrigin.Begin);
            var jsonContent = await new StreamReader(responseStream).ReadToEndAsync();
            var jsonBytes = Encoding.UTF8.GetBytes(jsonContent);

            // 压缩内容
            byte[] compressedBytes = preferredEncoding switch
            {
                "gzip" => CompressWithGzip(jsonBytes),
                "deflate" => CompressWithDeflate(jsonBytes),
                _ => jsonBytes
            };

            // 生成ETag
            var etag = $"\"{GenerateHash(Convert.ToBase64String(compressedBytes))}\"";

            // 准备缓存的响应头
            var headersToCache = new Dictionary<string, string>
            {
                { "ETag", etag },
                { "Content-Type", context.Response.ContentType },
                { "Content-Encoding", preferredEncoding }
            };

            // 缓存压缩后的响应
            await _cache.CacheResponseAsync(cacheKey, preferredEncoding, compressedBytes, headersToCache);

            // 返回最终响应
            foreach (var header in headersToCache)
            {
                context.Response.Headers.TryAdd(header.Key, header.Value);
            }
            context.Response.ContentLength = compressedBytes.Length;
            await originalBody.WriteAsync(compressedBytes);
        }
        finally
        {
            context.Response.Body = originalBody;
        }
    }

    #region 辅助方法
    private string GenerateHash(string input)
    {
        using var sha256 = SHA256.Create();
        var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
        return Convert.ToBase64String(hashBytes);
    }

    private string GetPreferredCompression(string acceptEncoding)
    {
        if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase))
            return "gzip";
        if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase))
            return "deflate";
        return "identity";
    }

    private byte[] CompressWithGzip(byte[] input)
    {
        using var outputStream = new MemoryStream();
        using var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal);
        gzipStream.Write(input, 0, input.Length);
        gzipStream.Close();
        return outputStream.ToArray();
    }

    private byte[] CompressWithDeflate(byte[] input)
    {
        using var outputStream = new MemoryStream();
        using var deflateStream = new DeflateStream(outputStream, CompressionLevel.Optimal);
        deflateStream.Write(input, 0, input.Length);
        deflateStream.Close();
        return outputStream.ToArray();
    }
    #endregion
}

3. 注册服务与中间件

在Program.cs中按正确顺序注册服务和中间件(必须放在授权之后):

var builder = WebApplication.CreateBuilder(args);

// 注册自研缓存服务
builder.Services.AddSingleton<ICustomResponseCache, InMemoryCustomResponseCache>();

// 添加MVC服务
builder.Services.AddControllersWithViews();

var app = builder.Build();

// 中间件顺序:静态文件 → 路由 → 认证 → 授权 → 自定义缓存 → MVC
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

// 注册自定义缓存压缩中间件
app.UseMiddleware<CustomCacheCompressionMiddleware>();

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

app.Run();

关键注意事项

  • 中间件顺序:必须在UseAuthorization之后执行,确保认证授权完成后再处理缓存,避免未授权用户获取缓存内容。
  • 缓存键扩展:可根据需求在RequestSignature中添加更多字段(如Accept-Language、自定义请求头),确保缓存键的唯一性。
  • 缓存策略:自研缓存实现需添加过期、清理逻辑(如定时删除旧缓存),避免内存/存储溢出。
  • ETag规范:生成的ETag需符合HTTP标准,通常带双引号,同时处理If-None-Match头以返回304。
  • 压缩优化:可使用异步压缩方法提升性能,或引入第三方压缩库(如Brotli)。

内容的提问来源于stack exchange,提问作者Bugmaster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 15:23:14