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状态,而我想要它实际上什么都没有改变.
任何人都可以指导我这个方向吗?提前致谢.
我的要求是缓存我的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)中
我喜欢@Viezevingertjes 提供的答案。这是最优雅且“无需设置任何东西”的方法,非常方便。我也喜欢这个 :)
但我认为它有一些缺点:
而且这不是问题的一部分,也没有人提到它。但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)
我发现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)
无需设置任何东西,设置和忘记.我喜欢它的方式.:)
| 归档时间: |
|
| 查看次数: |
21184 次 |
| 最近记录: |