filtros - Crear filtro ETag en ASP.NET MVC
paginacion mvc razor (4)
Me gustaría crear un filtro ETag en MVC. El problema es que no puedo controlar el Response.OutputStream; si pudiera hacerlo, simplemente calcularía el ETag de acuerdo con el flujo de resultados. Hice esto antes en WCF pero no pude encontrar una idea simple para hacer eso en MVC.
Quiero poder escribir algo así
[ETag]
public ActionResult MyAction()
{
var myModel = Factory.CreateModel();
return View(myModel);
}
¿Alguna idea?
Esto es lo mejor que se me ocurrió, realmente no entendí lo que querías decir con que no puedes controlar el 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);
}
}
Esto debería funcionar, pero no es así.
Aparentemente Microsoft anuló System.Web.HttpResponseStream.Read (Byte [] buffer, Int32 offset, Int32 count) para que devuelva "El método especificado no es compatible.", No estoy seguro de por qué lo harían, ya que hereda para el Sistema. Clase base IO.Stream ...
Lo cual es una mezcla de los siguientes recursos, Response.OutputStream es una secuencia de solo escritura, por lo que debemos usar una clase Response.Filter para leer la secuencia de salida, algo peculiar que tiene que usar un filtro en un filtro, pero funciona =)
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
Actualizar
Después de mucha lucha pude finalmente hacer que esto funcione:
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)));
}
}
Más recursos:
http://authors.aspalliance.com/aspxtreme/sys/Web/HttpResponseClassFilter.aspx
http://forums.asp.net/t/1380989.aspx/1
Hay bastantes respuestas prometedoras. Pero ninguno de ellos es una solución completa. Además, no era parte de la pregunta y nadie lo mencionó. Pero ETag se debe usar para la validación de caché. Por lo tanto, se debe utilizar con el encabezado Cache-Control . De modo que los clientes ni siquiera tienen que llamar al servidor hasta que caduque (puede ser un período de tiempo muy corto que depende de su recurso). Cuando el caché expiró, el cliente realiza una solicitud con ETag y lo valida. Para obtener más detalles sobre el almacenamiento en caché, consulte este artículo .
Aquí está mi solución de atributo CacheControl con ETags. Se puede mejorar, por ejemplo, con la caché pública habilitada, etc. Sin embargo, le recomiendo encarecidamente que comprenda el almacenamiento en caché y lo modifique con cuidado. Si usa HTTPS y los puntos finales están seguros, esta configuración debería estar bien.
/// <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("-", "");
}
}
}
Uso, por ejemplo: con 1 minuto de almacenamiento en caché del lado del cliente:
[ClientCacheWithEtag(60)]
Muchas gracias es exactamente lo que estaba buscando. Acabo de hacer una pequeña corrección al ETagFilter que manejará 304 en caso de que el contenido no haya cambiado
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";
}
}
}
este es el código que creé para resolver este problema - heredo de gzip porque también quiero gzip la corriente (siempre puedes usar una transmisión regular) la diferencia es que calculo el etag para toda mi respuesta y no solo un pedazo de eso.
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();
}
}