如何使用动作filter和HttpResponseMessage在Web API中使用ETag

我有一个ASP.Net Web API控制器,它只返回用户列表。

public sealed class UserController : ApiController { [EnableTag] public HttpResponseMessage Get() { var userList= this.RetrieveUserList(); // This will return list of users this.responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ObjectContent<List>(userList, new JsonMediaTypeFormatter()) }; return this.responseMessage; } } 

和一个动作filter属性类EnableTag ,负责管理ETag和缓存:

 public class EnableTag : System.Web.Http.Filters.ActionFilterAttribute { private static ConcurrentDictionary etags = new ConcurrentDictionary(); public override void OnActionExecuting(HttpActionContext context) { if (context != null) { var request = context.Request; if (request.Method == HttpMethod.Get) { var key = GetKey(request); ICollection etagsFromClient = request.Headers.IfNoneMatch; if (etagsFromClient.Count > 0) { EntityTagHeaderValue etag = null; if (etags.TryGetValue(key, out etag) && etagsFromClient.Any(t => t.Tag == etag.Tag)) { context.Response = new HttpResponseMessage(HttpStatusCode.NotModified); SetCacheControl(context.Response); } } } } } public override void OnActionExecuted(HttpActionExecutedContext context) { var request = context.Request; var key = GetKey(request); EntityTagHeaderValue etag; if (!etags.TryGetValue(key, out etag) || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) { etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\""); etags.AddOrUpdate(key, etag, (k, val) => etag); } context.Response.Headers.ETag = etag; SetCacheControl(context.Response); } private static void SetCacheControl(HttpResponseMessage response) { response.Headers.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(60), MustRevalidate = true, Private = true }; } private static string GetKey(HttpRequestMessage request) { return request.RequestUri.ToString(); } } 

上面的代码创建了一个属性类来管理ETag。 因此,在第一次请求时,它将创建一个新的E-Tag,并且对于后续请求,它将检查是否存在任何ETag。 如果是这样,它将生成Not Modified HTTP状态并返回给客户端。

我的问题是,如果我的用户列表中有更改,我想创建一个新的ETag,例如。 添加新用户,或删除现有用户。 并附上回复。 这可以通过userList变量进行跟踪。

目前,从客户端和服务器收到的ETag在每个第二个请求中都是相同的,所以在这种情况下它将始终生成Not Modified状态,而我想要它实际上没有任何改变。

任何人都可以指导我这个方向吗? 提前致谢。

我的要求是缓存我的web api JSON响应……并且所提供的所有解决方案都没有简单的“链接”到生成数据的位置 – 即在Controller中…

所以我的解决方案是创建一个生成响应的包装器“CacheableJsonResult”,然后将ETag添加到标头中。 这允许在生成控制器方法时传入etag并想要返回内容…

 public class CacheableJsonResult : JsonResult { private readonly string _eTag; private const int MaxAge = 10; //10 seconds between requests so it doesn't even check the eTag! public CacheableJsonResult(T content, JsonSerializerSettings serializerSettings, Encoding encoding, HttpRequestMessage request, string eTag) :base(content, serializerSettings, encoding, request) { _eTag = eTag; } public override Task ExecuteAsync(System.Threading.CancellationToken cancellationToken) { Task response = base.ExecuteAsync(cancellationToken); return response.ContinueWith((prior) => { HttpResponseMessage message = prior.Result; message.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", _eTag)); message.Headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromSeconds(MaxAge) }; return message; }, cancellationToken); } } 

然后,在您的控制器中 – 返回此对象:

 [HttpGet] [Route("results/{runId}")] public async Task GetRunResults(int runId) { //Is the current cache key in our cache? //Yes - return 304 //No - get data - and update CacheKeys string tag = GetETag(Request); string cacheTag = GetCacheTag("GetRunResults"); //you need to implement this map - or use Redis if multiple web servers if (tag == cacheTag ) return new StatusCodeResult(HttpStatusCode.NotModified, Request); //Build data, and update Cache... string newTag = "123"; //however you define this - I have a DB auto-inc ID on my messages //Call our new CacheableJsonResult - and assign the new cache tag return new CacheableJsonResult(results, GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings, System.Text.UTF8Encoding.Default, Request, newTag); } } private static string GetETag(HttpRequestMessage request) { IEnumerable values = null; if (request.Headers.TryGetValues("If-None-Match", out values)) return new EntityTagHeaderValue(values.FirstOrDefault()).Tag; return null; } 

您需要定义制作标签的细化程度; 我的数据是特定于用户的,因此我将UserId包含在CacheKey(etag)中

ETag和ASP.NET Web API的一个很好的解决方案是使用CacheCow 。 这里有一篇好文章。

它易于使用,您不必创建自定义属性。 玩得开心.u

我发现CacheCow非常臃肿,如果唯一的原因是,为了降低传输的数据量,你可能想要使用这样的东西:

 public class EntityTagContentHashAttribute : ActionFilterAttribute { private IEnumerable _receivedEntityTags; private readonly HttpMethod[] _supportedRequestMethods = { HttpMethod.Get, HttpMethod.Head }; public override void OnActionExecuting(HttpActionContext context) { if (!_supportedRequestMethods.Contains(context.Request.Method)) throw new HttpResponseException(context.Request.CreateErrorResponse(HttpStatusCode.PreconditionFailed, "This request method is not supported in combination with ETag.")); var conditions = context.Request.Headers.IfNoneMatch; if (conditions != null) { _receivedEntityTags = conditions.Select(t => t.Tag.Trim('"')); } } public override void OnActionExecuted(HttpActionExecutedContext context) { var objectContent = context.Response.Content as ObjectContent; if (objectContent == null) return; var computedEntityTag = ComputeHash(objectContent.Value); if (_receivedEntityTags.Contains(computedEntityTag)) { context.Response.StatusCode = HttpStatusCode.NotModified; context.Response.Content = null; } context.Response.Headers.ETag = new EntityTagHeaderValue("\"" + computedEntityTag + "\"", true); } private static string ComputeHash(object instance) { var cryptoServiceProvider = new MD5CryptoServiceProvider(); var serializer = new DataContractSerializer(instance.GetType()); using (var memoryStream = new MemoryStream()) { serializer.WriteObject(memoryStream, instance); cryptoServiceProvider.ComputeHash(memoryStream.ToArray()); return String.Join("", cryptoServiceProvider.Hash.Select(c => c.ToString("x2"))); } } } 

无需设置任何东西,设置和忘记。 我喜欢它的方式。 🙂

我喜欢@Viezevingertjes提供的答案。 它是最优雅的“不需要设置任何东西”的方法非常方便。 我也喜欢这个 :)

但是我认为它有一些缺点:

  • 整个OnActionExecuting()方法和在_receivedEntityTags中存储ETag是不必要的,因为Request也可以在OnActionExecuted方法中使用。
  • 仅适用于ObjectContent响应类型。
  • 由于序列化导致额外的工作量。

它也不是问题的一部分,没有人提到它。 但ETag应该用于缓存validation 。 因此,它应该与Cache-Control标头一起使用,以便客户端甚至不必在缓存过期之前调用服务器(它可能是非常短的时间段取决于您的资源)。 当缓存过期时,客户端使用ETag发出请求并validation它。 有关缓存的更多详细信息, 请参阅此文章 。

所以这就是为什么我决定把它拉出来一点但是。 简化filter不需要OnActionExecuting方法,适用于任何响应类型,无序列化。 最重要的是还添加了CacheControl标头。 它可以改进,例如启用公共缓存等… 但是我强烈建议您理解缓存并仔细修改它。 如果您使用HTTPS并且端点是安全的,那么此设置应该没问题。

 ///  /// Enables HTTP Response CacheControl management with ETag values. ///  public class ClientCacheWithEtagAttribute : ActionFilterAttribute { private readonly TimeSpan _clientCache; private readonly HttpMethod[] _supportedRequestMethods = { HttpMethod.Get, HttpMethod.Head }; ///  /// Default constructor ///  /// Indicates for how long the client should cache the response. The value is in seconds public ClientCacheWithEtagAttribute(int clientCacheInSeconds) { _clientCache = TimeSpan.FromSeconds(clientCacheInSeconds); } public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) { if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method)) { return; } if (actionExecutedContext.Response?.Content == null) { return; } var body = await actionExecutedContext.Response.Content.ReadAsStringAsync(); if (body == null) { return; } var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body)); if (actionExecutedContext.Request.Headers.IfNoneMatch.Any() && actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase)) { actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified; actionExecutedContext.Response.Content = null; } var cacheControlHeader = new CacheControlHeaderValue { Private = true, MaxAge = _clientCache }; actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false); actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader; } private static string GetETag(byte[] contentBytes) { using (var md5 = MD5.Create()) { var hash = md5.ComputeHash(contentBytes); string hex = BitConverter.ToString(hash); return hex.Replace("-", ""); } } } 

用法例如:1分钟客户端缓存:

 [ClientCacheWithEtag(60)] 

似乎是一个很好的方法:

 public class CacheControlAttribute : System.Web.Http.Filters.ActionFilterAttribute { public int MaxAge { get; set; } public CacheControlAttribute() { MaxAge = 3600; } public override void OnActionExecuted(HttpActionExecutedContext context) { if (context.Response != null) { context.Response.Headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromSeconds(MaxAge) }; context.Response.Headers.ETag = new EntityTagHeaderValue(string.Concat("\"", context.Response.Content.ReadAsStringAsync().Result.GetHashCode(), "\""),true); } base.OnActionExecuted(context); } }