如何使用动作过滤器和HttpResponseMessage在Web API中使用ETag

Sof*_*San 17 c# asp.net-web-api

我有一个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;
       }
}
Run Code Online (Sandbox Code Playgroud)

和一个动作过滤器属性类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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Jam*_*yce 6

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

因此,我的解决方案是创建一个包装器“ 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

[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;
}
Run Code Online (Sandbox Code Playgroud)

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


Maj*_*jor 6

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

但我认为它有一些缺点:

  • 整个 OnActionExecuting() 方法以及将 ETag 存储在 _receivedEntityTags 中是不必要的,因为 Request 在OnActionExecuted方法中也可用。
  • 仅适用于ObjectContent响应类型。
  • 由于序列化而产生额外的工作负载。

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

所以这就是为什么我决定稍微修饰一下。简化的过滤器不需要 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("-", "");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

[ClientCacheWithEtag(60)]
Run Code Online (Sandbox Code Playgroud)


小智 5

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

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


Vie*_*jes 5

我发现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")));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

无需设置任何东西,设置和忘记.我喜欢它的方式.:)