2013-11-22 15 views
11

Ho un controller API Web ASP.Net che restituisce semplicemente l'elenco di utenti.Come utilizzare ETag in Web API utilizzando il filtro azione insieme a HttpResponseMessage

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; 
     } 
} 

e un filtro azione attributo class EnableTag che è responsabile per la gestione e la cache 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(); 
    } 
} 

Il codice precedente crea una classe attributo per gestire ETag. Quindi, alla prima richiesta, creerà un nuovo E-Tag e per la successiva richiesta controllerà se esiste ETag. in caso affermativo, genererà lo stato HTTP Not Modified e tornerà al client.

Il mio problema è, voglio creare un nuovo ETag se ci sono cambiamenti nella mia lista utenti, es. viene aggiunto un nuovo utente o viene eliminato un utente esistente. e aggiungilo alla risposta. Questo può essere tracciato dalla variabile userList.

Attualmente, l'ETag ricevuto dal client e dal server è lo stesso da ogni seconda richiesta, quindi in questo caso genererà sempre lo stato Not Modified, mentre lo voglio quando effettivamente non è cambiato nulla.

Qualcuno può guidarmi in questa direzione? Grazie in anticipo.

risposta

2

La mia esigenza era quella di memorizzare nella cache i miei API Web risposte JSON ... E tutte le soluzioni fornite non hanno un "link" facile a dove i dati sono generati - cioè nel controller ...

Quindi la mia soluzione era creare un wrapper "CacheableJsonResult" che generasse una risposta, e quindi aggiunse l'ETag all'intestazione. Questo permette un etag da passare in quando il metodo di controllo è generato e che vuole restituire il contenuto ...

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); 
    } 
} 

E poi, nel controller - restituire questo oggetto:

[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; 
} 

È necessario per definire in che modo granulare creare i tag; i miei dati sono specifici dell'utente, quindi includo l'ID utente in CacheKey (etag)

+0

Questo è perfetto: offre una soluzione etag per le chiamate webapi senza la necessità di bufferizzare la risposta, riducendo le prestazioni, come fanno le soluzioni di checksum. –

5

una buona soluzione per ETag e in API Web ASP.NET è utilizzare CacheCow. Un buon articolo è here.

È facile da usare e non è necessario creare un attributo personalizzato. Buon divertimento .U

5

ho trovato CacheCow molto gonfio per quello che fa, se l'unica ragione è, per abbassare la quantità di dati trasferiti, si potrebbe desiderare di usare qualcosa di simile:

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"))); 
     } 
    } 
} 

Non è necessario impostare nulla, impostare e dimenticare. Il modo in cui mi piace :)

+1

Sì penso che questo approccio è il migliore. Grazie per la risposta @Viezevingertjes è stato utile. Comunque secondo me ci sono un paio di cose che possono essere migliorate. Così ho usato il tuo codice e l'ho modificato: https://stackoverflow.com/questions/20145140/how-to-use-etag-in-web-api-using-action-filter-along-with-httpresponsemessage/49169225# 49169225 – Major

-1

sembra essere un bel modo per farlo:

public class CacheControlAttribute : System.Web.Http.Filters.ActionFilterAttribute 
{ 
    public int MaxAge { get; set; } 

    public CacheControlAttribute() 
    { 
     MaxAge = 3600; 
    } 

    public override void OnActionExecuted(HttpActionExecutedContext context) 
    { 
     if (context.Response != null) 
     { 
      context.Response.Headers.CacheControl = new CacheControlHeaderValue 
      { 
       Public = true, 
       MaxAge = TimeSpan.FromSeconds(MaxAge) 
      }; 
      context.Response.Headers.ETag = new EntityTagHeaderValue(string.Concat("\"", context.Response.Content.ReadAsStringAsync().Result.GetHashCode(), "\""),true); 
     } 
     base.OnActionExecuted(context); 
    } 
} 
0

Mi piace la risposta fornita da @Viezevingertjes. È l'approccio più elegante e "Non c'è bisogno di impostare nulla" è molto conveniente.Mi piace troppo :)

Tuttavia Credo che abbia alcuni inconvenienti:

  • Tutta OnActionExecuting() e ETags memorizzazione in _receivedEntityTags è inutile perché la richiesta è disponibile all'interno della OnActionExecuted Procedimento bene.
  • Funziona solo con i tipi di risposta ObjectContent.
  • Carico di lavoro aggiuntivo a causa della serializzazione.

Inoltre non era parte della domanda e nessuno lo ha menzionato. Ma ETag deve essere utilizzato per la convalida della cache. Pertanto dovrebbe essere usato con l'intestazione Cache-Control in modo che i client non debbano nemmeno chiamare il server fino alla scadenza della cache (può essere un periodo di tempo molto breve dipende dalla risorsa). Quando la cache è scaduta, il client effettua una richiesta con ETag e la convalida. Per ulteriori dettagli sulla memorizzazione nella cache see this article.

Ecco perché ho deciso di sfruttarlo un po 'ma. Il filtro semplificato non richiede il metodo OnActionExecuting, funziona con qualsiasi tipo di risposta, nessuna serializzazione. E, soprattutto, aggiunge l'intestazione di CacheControl. Può essere migliorato per es. con cache pubblica abilitata, ecc ... Tuttavia, ti consiglio vivamente di capire la memorizzazione nella cache e di modificarla attentamente. Se si utilizza HTTPS e gli endpoint sono protetti, questa configurazione dovrebbe andare bene.

/// <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 es: con 1 min di caching lato client:

[ClientCacheWithEtag(60)] 
Problemi correlati