← 返回首页目录
# ASP.NET Web API 输出缓存实现全指南

## 核心概念

在构建高性能的Web API服务时,输出缓存是一项至关重要的优化技术。通过缓存API响应,可以显著减少服务器负载,提升响应速度,改善用户体验。然而,许多开发者在从传统MVC控制器迁移到Web API时,会遇到一个常见的陷阱:**`[OutputCache]`属性在Web API中并不直接适用**。

Web API是ASP.NET框架中专门用于构建RESTful服务的组件,它与传统MVC在架构设计上存在根本性差异。传统MVC中的`[OutputCache]`属性位于`System.Web.Mvc`命名空间下,依赖于`HttpContext.Current`和完整的ASP.NET管道,而Web API则基于`System.Net.Http`和`HttpRequestMessage`/`HttpResponseMessage`模型设计,两者的缓存机制完全不同。

本文旨在深入剖析Web API的缓存机制,揭示为什么传统缓存方案失效,并提供多种经过验证的实现方案,帮助开发者在Web API项目中成功实现输出缓存。

## 问题根源分析

### 缓存机制的根本差异

当开发者在Web API控制器中尝试使用`[OutputCache]`属性时,会遇到一个令人困惑的现象:响应头中明确显示"Cache-Control: no-cache"和"Pragma: no-cache",表明缓存被完全禁用。这并非使用错误,而是Web API框架设计上的有意安排。

Web API团队在设计之初就做出了一个关键决策:**默认情况下禁用所有输出缓存**。这一决定基于以下几个考量:

1. **RESTful架构原则**:REST服务通常要求无状态通信,缓存可能会引入状态一致性风险
2. **API响应特性**:许多API响应是动态生成的,包含用户特定数据或实时信息
3. **安全考虑**:缓存可能泄漏敏感数据,尤其在多租户环境中
4. **HTTP规范遵循**:Web API更严格遵循HTTP规范,要求显式设置缓存策略

### 框架源码层面的证据

通过对ASP.NET Web Stack源代码的分析,可以在`HttpControllerHandler`类中看到明确的缓存禁用逻辑。以下是关键代码片段:

```csharp
CacheControlHeaderValue cacheControl = response.Headers.CacheControl;
if (cacheControl == null)
{
    // ASP.NET默认会发送cache-control: private头
    // 但Web API不希望请求被默认缓存
    // 如果没有人显式设置CacheControl,则强制设置为no-cache
    httpContextBase.Response.Cache.SetCacheability(HttpCacheability.NoCache);
}
```

这段代码揭示了一个重要事实:**只要响应头中的`CacheControl`属性为null,Web API框架就会强制设置缓存为不可用**。这是开发者看到no-cache响应的根本原因。

### 响应的HTTP头分析

当未设置缓存时,典型的响应头如下:

```
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/xml; charset=utf-8
```

这些头信息共同作用,向客户端和中间代理服务器明确指示:禁止缓存此响应内容。浏览器、CDN和代理服务器都会严格遵守这些指令。

## 解决方案一:手动设置缓存头

### 基本实现方法

最直接的控制方法是在API控制器中手动设置HTTP缓存响应头。通过修改响应消息的`CacheControl`属性,可以精确控制缓存行为:

```csharp
using System.Net.Http;
using System.Net.Http.Headers;

public class TestController : ApiController
{
    public HttpResponseMessage Get()
    {
        var response = Request.CreateResponse(HttpStatusCode.OK);
        response.Content = new StringContent(System.DateTime.Now.ToString());
        
        // 创建并配置缓存控制头
        response.Headers.CacheControl = new CacheControlHeaderValue
        {
            MaxAge = TimeSpan.FromMinutes(10),  // 缓存有效期10分钟
            Public = true                       // 允许公共缓存(CDN、代理等)
        };
        
        return response;
    }
}
```

### 缓存控制头的详细配置

`CacheControlHeaderValue`对象提供了丰富的配置选项,开发者可以根据需求灵活组合:

**缓存可见性配置**:
- `Public = true`:允许任何中间缓存(CDN、代理)缓存响应
- `Private = true`:仅允许客户端浏览器缓存
- `NoCache = true`:禁用缓存,但会进行条件验证(ETag)

**过期策略配置**:
- `MaxAge = TimeSpan.FromSeconds(600)`:设置绝对过期时间
- `SharedMaxAge = TimeSpan.FromSeconds(300)`:为共享缓存设置单独的过期时间
- `MustRevalidate = true`:强制缓存在使用过期内容前必须验证

**其他重要属性**:
- `NoStore = true`:完全不存储任何缓存(适用于敏感数据)
- `NoTransform = true`:禁止代理修改内容(如图片压缩)
- `ProxyRevalidate = true`:类似于MustRevalidate,但针对代理缓存

### 效果验证

正确配置后,响应头将显示:

```
Cache-Control: public, max-age=600
Content-Type: text/plain; charset=utf-8
Date: Wed, 13 Mar 2013 21:06:10 GMT
```

此时,客户端和中间代理都会根据`max-age`指令缓存响应内容。

## 解决方案二:自定义ActionFilter属性

### 创建可重用的缓存属性

为了提升代码的可维护性和复用性,更优雅的方案是创建一个自定义的ActionFilter属性。这种方案遵循了面向切面编程(AOP)的原则,将缓存逻辑与业务逻辑分离:

```csharp
public class CacheWebApiAttribute : ActionFilterAttribute
{
    public int Duration { get; set; }  // 缓存持续时间(分钟)
    public bool IsPublic { get; set; } // 是否允许公共缓存
    public bool MustRevalidate { get; set; } // 是否强制重新验证

    public override void OnActionExecuted(HttpActionExecutedContext filterContext)
    {
        // 确保响应对象存在
        if (filterContext.Response == null)
            return;

        // 创建缓存控制头配置
        var cacheControl = new CacheControlHeaderValue
        {
            MaxAge = TimeSpan.FromMinutes(Duration),
            MustRevalidate = MustRevalidate,
            Private = !IsPublic,
            Public = IsPublic
        };

        // 应用到响应
        filterContext.Response.Headers.CacheControl = cacheControl;
    }
}
```

### 属性使用示例

```csharp
[CacheWebApi(Duration = 20, IsPublic = true)]
public IEnumerable Get()
{
    return new string[] 
    { 
        DateTime.Now.ToLongTimeString(), 
        DateTime.UtcNow.ToLongTimeString() 
    };
}
```

### 高级配置选项

可以扩展属性以支持更复杂的缓存策略:

```csharp
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class AdvancedCacheAttribute : ActionFilterAttribute
{
    public int Duration { get; set; }
    public string VaryByHeader { get; set; }
    public bool VaryByUser { get; set; }

    public override void OnActionExecuted(HttpActionExecutedContext context)
    {
        var cacheControl = new CacheControlHeaderValue
        {
            MaxAge = TimeSpan.FromMinutes(Duration),
            Public = true
        };
        
        context.Response.Headers.CacheControl = cacheControl;
        
        // 添加Vary头,实现基于请求头的缓存差异化
        if (!string.IsNullOrEmpty(VaryByHeader))
        {
            context.Response.Headers.Vary.Add(VaryByHeader);
        }
        
        // 按用户缓存
        if (VaryByUser)
        {
            context.Response.Headers.Vary.Add("Authorization");
            
            // 或者使用自定义缓存键
            var cacheKey = $"user_{Thread.CurrentPrincipal.Identity.Name}";
            // 实现自定义缓存逻辑
        }
    }
}
```

## 解决方案三:使用DelegatingHandler实现全局缓存

### 管道处理模型的理解

Web API的消息处理管道基于`DelegatingHandler`模式构建,允许在每个请求或响应流经管道时执行自定义逻辑。这种机制为全局缓存提供了完美的切入点:

```csharp
public class CachingHandler : DelegatingHandler
{
    private readonly ICacheProvider _cacheProvider;

    public CachingHandler(ICacheProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }

    protected override async Task SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 只处理GET请求
        if (request.Method == HttpMethod.Get)
        {
            var cacheKey = GenerateCacheKey(request);
            
            // 尝试从缓存获取
            var cachedResponse = _cacheProvider.Get(cacheKey);
            if (cachedResponse != null)
            {
                return cachedResponse;
            }

            // 执行实际请求
            var response = await base.SendAsync(request, cancellationToken);
            
            // 缓存响应
            if (response.IsSuccessStatusCode)
            {
                _cacheProvider.Set(cacheKey, response, TimeSpan.FromMinutes(10));
            }

            return response;
        }

        return await base.SendAsync(request, cancellationToken);
    }

    private string GenerateCacheKey(HttpRequestMessage request)
    {
        // 基于URL和查询参数生成唯一缓存键
        return request.RequestUri.ToString();
    }
}

// 缓存提供者接口
public interface ICacheProvider
{
    HttpResponseMessage Get(string key);
    void Set(string key, HttpResponseMessage response, TimeSpan duration);
}
```

### 注册全局Handler

在`WebApiConfig.cs`中注册处理程序:

```csharp
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // 注册自定义缓存处理程序
        config.MessageHandlers.Add(new CachingHandler(new MemoryCacheProvider()));
        
        // 其他配置...
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
```

### 缓存实现策略对比

| 实现方式 | 优点 | 缺点 | 适用场景 |
|---------|------|------|----------|
| 手动设置头 | 简单直接,控制粒度细 | 代码冗余,不易维护 | 单个或少数API方法 |
| ActionFilter | 声明式编程,可复用 | 作用域受限 | 特定控制器或方法 |
| DelegatingHandler | 全局统一管理,逻辑集中 | 复杂度较高 | 企业级应用,统一缓存策略 |

## 最佳实践与高级策略

### ETag和条件请求

结合ETag实现更高效的缓存验证机制:

```csharp
public class ETagHandler : DelegatingHandler
{
    protected override async Task SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);
        
        if (response.IsSuccessStatusCode)
        {
            // 生成ETag(基于内容哈希)
            var content = await response.Content.ReadAsStringAsync();
            var eTag = $"\"{ComputeHash(content)}\"";
            
            response.Headers.ETag = new EntityTagHeaderValue(eTag);
            
            // 检查If-None-Match头
            var ifNoneMatch = request.Headers.IfNoneMatch;
            if (ifNoneMatch.Any(h => h.Tag == eTag))
            {
                return new HttpResponseMessage(HttpStatusCode.NotModified);
            }
        }
        
        return response;
    }
    
    private string ComputeHash(string content)
    {
        using (var md5 = System.Security.Cryptography.MD5.Create())
        {
            var bytes = Encoding.UTF8.GetBytes(content);
            var hash = md5.ComputeHash(bytes);
            return BitConverter.ToString(hash).Replace("-", "").ToLower();
        }
    }
}
```

### 缓存依赖与失效策略

1. **时间驱动失效**:基于固定时间间隔(TTL)
2. **事件驱动失效**:当相关资源更新时清除缓存
3. **显式失效**:通过管理API手动清除

```csharp
public class CacheManager
{
    private static readonly ConcurrentDictionary _cacheDependencies 
        = new ConcurrentDictionary();

    public static void InvalidateByResource(string resourceType)
    {
        // 清除依赖于特定资源类型的所有缓存
        var keysToRemove = _cacheDependencies
            .Where(kvp => kvp.Value == resourceType)
            .Select(kvp => kvp.Key);
            
        foreach (var key in keysToRemove)
        {
            MemoryCache.Default.Remove(key);
        }
    }
}
```

### 性能监控与调优

实施缓存策略后,需要持续监控缓存命中率、响应时间和服务器负载:

1. **缓存命中率**:理想值应在80%以上
2. **响应时间**:缓存后应降低1-2个数量级
3. **内存使用**:避免缓存过大导致内存压力
4. **序列化开销**:复杂对象的序列化可能抵消缓存收益

## 结论

Web API的输出缓存实现需要开发者深入理解框架的缓存机制和HTTP协议规范。核心要点包括:

1. **默认禁用**:Web API默认禁用了所有缓存,需要显式配置才能启用
2. **手动设置**:最基本的方案是在响应中手动设置`CacheControl`头
3. **属性封装**:通过自定义ActionFilter或DelegatingHandler实现可复用的缓存策略
4. **全局策略**:考虑使用DelegatingHandler实现统一的缓存管理
5. **条件缓存**:结合ETag和条件请求实现更高效的缓存验证

选择合适的缓存方案需要综合考虑应用架构、性能需求和维护成本。对于简单场景,手动设置即可满足需求;对于大型企业级应用,全局DelegatingHandler配合完善的缓存失效策略是更优选择。无论采用哪种方案,理解Web API缓存底层机制始终是实现高效缓存的基础。