在ASP.NET Core Web API中实现HTTP缓存(ETag)

Bri*_*rij 29 asp.net-web-api asp.net-core

我正在使用ASP.NET Core(ASP.NET 5)Web API应用程序,并且必须在实体标记的帮助下实现HTTP缓存.早些时候我使用了CacheCow,但它似乎不支持ASP.NET Core.我也没有找到任何其他相关的库或框架支持细节.

我可以编写相同的自定义代码,但在此之前我想看看是否有任何东西可用.如果某些东西已经可用,请分享,以及实施该方法的更好方法.

eri*_*zic 34

经过一段时间尝试使其与中间件一起工作后,我发现MVC动作过滤器实际上更适合这种功能.

public class ETagFilter : Attribute, IActionFilter
{
    private readonly int[] _statusCodes;

    public ETagFilter(params int[] statusCodes)
    {
        _statusCodes = statusCodes;
        if (statusCodes.Length == 0) _statusCodes = new[] { 200 };
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.HttpContext.Request.Method == "GET")
        {
            if (_statusCodes.Contains(context.HttpContext.Response.StatusCode))
            {
                //I just serialize the result to JSON, could do something less costly
                var content = JsonConvert.SerializeObject(context.Result);

                var etag = ETagGenerator.GetETag(context.HttpContext.Request.Path.ToString(), Encoding.UTF8.GetBytes(content));

                if (context.HttpContext.Request.Headers.Keys.Contains("If-None-Match") && context.HttpContext.Request.Headers["If-None-Match"].ToString() == etag)
                {
                    context.Result = new StatusCodeResult(304);
                }
                context.HttpContext.Response.Headers.Add("ETag", new[] { etag });
            }
        }
    }        
}

// Helper class that generates the etag from a key (route) and content (response)
public static class ETagGenerator
{
    public static string GetETag(string key, byte[] contentBytes)
    {
        var keyBytes = Encoding.UTF8.GetBytes(key);
        var combinedBytes = Combine(keyBytes, contentBytes);

        return GenerateETag(combinedBytes);
    }

    private static string GenerateETag(byte[] data)
    {
        using (var md5 = MD5.Create())
        {
            var hash = md5.ComputeHash(data);
            string hex = BitConverter.ToString(hash);
            return hex.Replace("-", "");
        }            
    }

    private static byte[] Combine(byte[] a, byte[] b)
    {
        byte[] c = new byte[a.Length + b.Length];
        Buffer.BlockCopy(a, 0, c, 0, a.Length);
        Buffer.BlockCopy(b, 0, c, a.Length, b.Length);
        return c;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在您想要作为属性的动作或控制器上使用它:

[HttpGet("data")]
[ETagFilter(200)]
public async Task<IActionResult> GetDataFromApi()
{
}
Run Code Online (Sandbox Code Playgroud)

Middleware和Filters之间的重要区别在于,您的中间件可以在MVC middlware之前和之后运行,并且只能与HttpContext一起使用.此外,一旦MVC开始将响应发送回客户端,对它进行任何更改都为时已晚.

另一方面,过滤器是MVC中间件的一部分.他们可以访问MVC上下文,在这种情况下,实现此功能更简单.更多关于过滤器及其在MVC中的管道.


sec*_*ean 5

基于Eric 的回答,我将使用一个可以在实体上实现的接口来支持实体标记。在过滤器中,如果操作返回具有此接口的实体,您将只添加 ETag。

这使您可以更有选择性地选择标记哪些实体,并允许您让每个实体控制其标记的生成方式。这比序列化所有内容并创建哈希要有效得多。它还消除了检查状态代码的需要。它可以安全轻松地作为全局过滤器添加,因为您通过在模型类上实现接口来“选择加入”该功能。

public interface IGenerateETag
{
    string GenerateETag();
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class ETagFilterAttribute : Attribute, IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        var request = context.HttpContext.Request;
        var response = context.HttpContext.Response;

        if (request.Method == "GET" &&
            context.Result is ObjectResult obj &&
            obj.Value is IGenerateETag entity)
        {
            string etag = entity.GenerateETag();

            // Value should be in quotes according to the spec
            if (!etag.EndsWith("\""))
                etag = "\"" + etag +"\"";

            string ifNoneMatch = request.Headers["If-None-Match"];

            if (ifNoneMatch == etag)
            {
                context.Result = new StatusCodeResult(304);
            }

            context.HttpContext.Response.Headers.Add("ETag", etag);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)