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
相关产品推荐
相关产品推荐

