我想在MVC中创建一个ETag过滤器。
问题是,我无法控制Response.OutputStream,如果能够控制,那么我将根据结果流计算ETag。
我之前在WCF中做过这件事,但找不到在MVC中实现的简单方法。
我想能够编写类似于以下的代码:
[ETag]
public ActionResult MyAction()
{
var myModel = Factory.CreateModel();
return View(myModel);
}
有任何想法吗?
我想在MVC中创建一个ETag过滤器。
问题是,我无法控制Response.OutputStream,如果能够控制,那么我将根据结果流计算ETag。
我之前在WCF中做过这件事,但找不到在MVC中实现的简单方法。
我想能够编写类似于以下的代码:
[ETag]
public ActionResult MyAction()
{
var myModel = Factory.CreateModel();
return View(myModel);
}
有任何想法吗?
这是我能想到的最好方案,但我并不真正理解你所说的不能控制Response.OutputStream的含义。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Web.Mvc;
public class ETagAttribute : ActionFilterAttribute
{
private string GetToken(Stream stream) {
MD5 md5 = MD5.Create();
byte [] checksum = md5.ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
filterContext.HttpContext.Response.AppendHeader("ETag", GetToken(filterContext.HttpContext.Response.OutputStream));
base.OnResultExecuted(filterContext);
}
}
这应该可以工作,但实际上并没有。
显然,Microsoft覆盖了System.Web.HttpResponseStream.Read(Byte[] buffer, Int32 offset, Int32 count)方法,以便它返回“指定的方法不受支持”。不确定为什么他们会这样做,因为它继承自System.IO.Stream基类...
这是以下资源的混合体,Response.OutputStream是一个只写流,所以我们必须使用Response.Filter类来读取输出流,有点奇怪,你必须在过滤器上使用一个过滤器,但它可以工作 =)
http://bytes.com/topic/c-sharp/answers/494721-md5-encryption-question-communication-java
http://www.codeproject.com/KB/files/Calculating_MD5_Checksum.aspx
http://blog.gregbrant.com/post/Adding-Custom-HTTP-Headers-to-an-ASPNET-MVC-Response.aspx
http://www.infoq.com/articles/etags
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
更新
经过长时间的奋斗,我终于成功让它工作了:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Web;
using System.Web.Mvc;
public class ETagAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
try {
filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response);
} catch (System.Exception) {
// Do Nothing
};
}
}
public class ETagFilter : MemoryStream {
private HttpResponseBase o = null;
private Stream filter = null;
public ETagFilter (HttpResponseBase response) {
o = response;
filter = response.Filter;
}
private string GetToken(Stream stream) {
byte[] checksum = new byte[0];
checksum = MD5.Create().ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void Write(byte[] buffer, int offset, int count) {
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
filter.Write(data, 0, count);
o.AddHeader("ETag", GetToken(new MemoryStream(data)));
}
}
更多资源:
http://authors.aspalliance.com/aspxtreme/sys/Web/HttpResponseClassFilter.aspx
http://forums.asp.net/t/1380989.aspx/1
非常感谢,这正是我在寻找的。我对ETagFilter进行了一点小修复,可以处理304状态码,以防内容没有更改。
public class ETagAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request);
}
}
public class ETagFilter : MemoryStream
{
private HttpResponseBase _response = null;
private HttpRequestBase _request;
private Stream _filter = null;
public ETagFilter(HttpResponseBase response, HttpRequestBase request)
{
_response = response;
_request = request;
_filter = response.Filter;
}
private string GetToken(Stream stream)
{
byte[] checksum = new byte[0];
checksum = MD5.Create().ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void Write(byte[] buffer, int offset, int count)
{
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
var token = GetToken(new MemoryStream(data));
string clientToken = _request.Headers["If-None-Match"];
if (token != clientToken)
{
_response.Headers["ETag"] = token;
_filter.Write(data, 0, count);
}
else
{
_response.SuppressContent = true;
_response.StatusCode = 304;
_response.StatusDescription = "Not Modified";
_response.Headers["Content-Length"] = "0";
}
}
}
/// <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)]
public class ETagFilter : GZipStream
{
private readonly HttpResponseBase m_Response;
private readonly HttpRequestBase m_Request;
private readonly MD5 m_Md5;
private bool m_FinalBlock;
public ETagFilter(HttpResponseBase response, HttpRequestBase request)
: base(response.Filter, CompressionMode.Compress)
{
m_Response = response;
m_Request = request;
m_Md5 = MD5.Create();
}
protected override void Dispose(bool disposing)
{
m_Md5.Dispose();
base.Dispose(disposing);
}
private string ByteArrayToString(byte[] arrInput)
{
var output = new StringBuilder(arrInput.Length);
for (var i = 0; i < arrInput.Length; i++)
{
output.Append(arrInput[i].ToString("X2"));
}
return output.ToString();
}
public override void Write(byte[] buffer, int offset, int count)
{
m_Md5.TransformBlock(buffer, 0, buffer.Length, null, 0);
base.Write(buffer, 0, buffer.Length);
}
public override void Flush()
{
if (m_FinalBlock)
{
base.Flush();
return;
}
m_FinalBlock = true;
m_Md5.TransformFinalBlock(new byte[0], 0, 0);
var token = ByteArrayToString(m_Md5.Hash);
string clientToken = m_Request.Headers["If-None-Match"];
if (token != clientToken)
{
m_Response.Headers["ETag"] = token;
}
else
{
m_Response.SuppressContent = true;
m_Response.StatusCode = 304;
m_Response.StatusDescription = "Not Modified";
m_Response.Headers["Content-Length"] = "0";
}
base.Flush();
}
}