Créer un filtre ETag ASP.NET MVC
je voudrais créer un filtre ETag dans MVC. Le problème est que je ne peux pas contrôler la réponse.OutputStream, si j'étais capable de le faire, je calculerais simplement L'ETag en fonction du flux de résultats. J'ai déjà fait ça à la WCF, mais je n'ai pas trouvé d'idée simple pour faire ça à MVC.
je veux être capable d'écrire quelque chose comme ça
[ETag]
public ActionResult MyAction()
{
var myModel = Factory.CreateModel();
return View(myModel);
}
une idée?
4 réponses
C'est le mieux que j'ai pu trouver, je n'ai pas vraiment compris ce que vous vouliez dire par vous ne pouvez pas contrôler la réponse.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);
}
}
Cela devrait fonctionner, mais qui ne fonctionne pas.
apparemment Microsoft a dépassé le système.Web.HttpResponseStream.Read (Byte[] buffer, Int32 offset, Int32 count) de sorte qu'il retourne "la méthode spécifiée n'est pas supportée.", pas sûr pourquoi ils feraient cela, car il hérite pour le système.IO.Classe de base des cours d'eau...
ce Qui est mélanger des ressources suivantes, la Réponse.OutputStream est un flux en écriture seulement, donc nous devons utiliser une réponse.Classe de filtre pour lire le flux de sortie, un peu bizarre que vous devez utiliser un filtre sur un filtre, mais il fonctionne =)
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
mise à Jour
Après beaucoup de combats j'ai enfin réussi à obtenir que cela fonctionne:
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)));
}
}
Plus De Ressources:
http://authors.aspalliance.com/aspxtreme/sys/Web/HttpResponseClassFilter.aspx
http://forums.asp.net/t/1380989.aspx/1
Merci beaucoup c'est exactement ce que je cherchais. Je viens de faire une petite correction à L'ETagFilter qui va gérer 304 dans le cas où le contenu n'a pas été changé
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";
}
}
}
il y a pas mal de réponses prometteuses. Mais aucune d'entre elles n'est une solution complète. Aussi fut-il pas partie de la question et personne n'a mentionné. Mais ETag doit être utilisé pour la validation du Cache. Par conséquent, il doit être utilisé avec Cache-Control header. Ainsi, les clients n'ont même pas besoin d'appeler le serveur jusqu'à ce que le cache expire (cela peut être une période très courte dépend de votre ressource). Lorsque le cache est expiré, le client fait une requête avec ETag et la valide. Pour plus de détails sur la mise en cache voir cet article.
voici ma solution d'attribut CacheControl avec ETags. Il peut être amélioré, par exemple en activant le cache Public, etc... cependant je vous conseille fortement de comprendre la mise en cache et de la modifier soigneusement. Si vous utilisez HTTPS et que les paramètres sont sécurisés, cette configuration devrait être correcte.
/// <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("-", "");
}
}
}
Usage E. G: avec 1 min de cache côté client:
[ClientCacheWithEtag(60)]
c'est le code que j'ai créé pour résoudre ce problème - je hériter de gzip parce que je veux gzip le flux comme bien ( vous pouvez toujours utiliser un flux régulier) la différence est que je calcule l'etag pour tous mes réponse et pas seulement partie.
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();
}
}