【问题标题】:How to use ETag in Web API using action filter along with HttpResponseMessage如何使用动作过滤器和 HttpResponseMessage 在 Web API 中使用 ETag
【发布时间】:2013-11-22 12:39:03
【问题描述】:

我有一个简单地返回用户列表的 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<UserViewModel>>(userList, new  JsonMediaTypeFormatter())
        };
        return this.responseMessage;
       }
}

还有一个动作过滤器属性类EnableTag负责管理ETag和缓存:

public class EnableTag : System.Web.Http.Filters.ActionFilterAttribute
{
    private static ConcurrentDictionary<string, EntityTagHeaderValue> etags = new ConcurrentDictionary<string, EntityTagHeaderValue>();

    public override void OnActionExecuting(HttpActionContext context)
    {
        if (context != null)
        {
            var request = context.Request;
            if (request.Method == HttpMethod.Get)
            {
                var key = GetKey(request);
                ICollection<EntityTagHeaderValue> 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 状态,而我想要它实际上没有任何变化。

谁能指导我朝这个方向发展?

【问题讨论】:

    标签: c# asp.net-web-api


    【解决方案1】:

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

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

    public class CacheableJsonResult<T> : JsonResult<T>
    {
        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<HttpResponseMessage> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
        {
            Task<HttpResponseMessage> response = base.ExecuteAsync(cancellationToken);
    
            return response.ContinueWith<HttpResponseMessage>((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<IHttpActionResult> 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<WebsiteRunResults>(results, GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings, System.Text.UTF8Encoding.Default, Request, newTag);
    
        }
    }
    
    private static string GetETag(HttpRequestMessage request)
    {
        IEnumerable<string> values = null;
        if (request.Headers.TryGetValues("If-None-Match", out values))
            return new EntityTagHeaderValue(values.FirstOrDefault()).Tag;
    
        return null;
    }
    

    您需要定义制作标签的粒度;我的数据是特定于用户的,所以我在 CacheKey (etag) 中包含 UserId

    【讨论】:

    • 这是完美的——它为 webapi 调用提供了一个 etag 解决方案,无需缓冲响应,降低了性能——就像校验和解决方案一样。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

      public class EntityTagContentHashAttribute : ActionFilterAttribute
      {
          private IEnumerable<string> _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 的回答,这很有帮助。但是在我看来,有几件事可以改进。所以我使用了你的代码并修改了它:stackoverflow.com/questions/20145140/…
      • @Major async 当时还不存在,但看起来是一个很好的补充。
      【解决方案4】:

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

      但我认为它有一些缺点:

      • 整个 OnActionExecuting() 方法和将 ETag 存储在 _receivedEntityTags 中是不必要的,因为请求在 OnActionExecuted 方法中也是可用的。
      • 仅适用于 ObjectContent 响应类型。
      • 由于序列化,工作量增加。

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

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

      /// <summary>
      /// Enables HTTP Response CacheControl management with ETag values.
      /// </summary>
      public class ClientCacheWithEtagAttribute : ActionFilterAttribute
      {
          private readonly TimeSpan _clientCache;
      
          private readonly HttpMethod[] _supportedRequestMethods = {
              HttpMethod.Get,
              HttpMethod.Head
          };
      
          /// <summary>
          /// Default constructor
          /// </summary>
          /// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
          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)]
      

      【讨论】:

      • 这是一个很好的默认设置,当您不介意总是产生结果,即使它可能无法交付。我会通过检查响应中现有的 Etag 来进一步完善它。然后,如果有更好的生成方法,任何想要实现自己的 etag 处理而不干扰默认实现的函数。
      【解决方案5】:

      似乎是一个不错的方法:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-09-03
        • 1970-01-01
        • 2013-12-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多