2013-11-22 6 views
11

私は、単純にユーザーのリストを返すASP.Net Web APIコントローラーを持っています。HttpResponseMessageと共にアクションフィルターを使用してWeb APIでETagを使用する方法

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

とのETagとキャッシュを管理する責任があるアクションフィルタ属性クラスEnableTag

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

上記のコードのETagを管理するための属性クラスを作成します。したがって、最初の要求では、新しいEタグが作成され、後続の要求に対しては、ETagが存在するかどうかがチェックされます。その場合は、Not Modified HTTPステータスが生成され、クライアントに戻ります。

私の問題は、ユーザーリストに変更があった場合、新しいETagを作成することです。新しいユーザーが追加されるか、既存のユーザーが削除されます。それに応答を追加します。これはuserList変数によって追跡できます。

現在のところ、クライアントとサーバーから受信したETagは、2番目の要求ごとに同じです。この場合、実際には何も変更されていないときには常に、Not Modifiedのステータスが生成されます。

誰でもこの方向に私を導くことができますか?前もって感謝します。

答えて

2

私の要件は...私のWeb APIのJSONレスポンスをキャッシュすることだったし、提供ソリューションをすべて簡単に「リンク」を持っていませんどこにデータが生成されたか、つまりコントローラーで...

私の解決策は、応答を生成したラッパー "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); 
    } 
} 

そして、あなたのコントローラに - このオブジェクトを返す:

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

あなたが必要タグを細かく作成する方法を定義します。私のデータはユーザ固有のものなので、私はCacheKey(etag)にUserIdを含めます

+0

これは完璧です - それは、パフォーマンスの減少、応答をバッファリングすることなくWEBAPI通話用のETagソリューションを提供 - などのチェックサムソリューションが行います。 –

5

ETagとASP.NET Web APIの良い解決策は、CacheCowを使用することです。良い記事はhereです。

使いやすく、カスタム属性を作成する必要はありません。 は、私が唯一の理由は、転送されるデータの量を下げるために、であるならば、あなたはこのようなものを使用する場合があります、それが何のためにCacheCowは非常に肥大化した楽しい .U

5

見つけました:

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

何も設定する必要はなく、設定して忘れてください。私はそれが好きな方法。 :)

+1

はい、私はこのアプローチは最高だと思います。 @Viezevingertjesのおかげでありがとうございました。しかし、私の意見では、改善できるものがいくつかあります。だから私はあなたのコードを使用し、それが参照修正:https://stackoverflow.com/questions/20145140/how-to-use-etag-in-web-api-using-action-filter-along-with-httpresponsemessage/49169225# 49169225 – Major

-1

はそれを行うには良い方法であるように思わ:

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

私は@Viezevingertjesによって提供された答えが好きです。それは最もエレガントな "何もセットアップする必要はありません"アプローチは非常に便利です。私もそれが好き:)

は、しかし、私はそれはいくつかの欠点を持っていると思う:

  • リクエストがとして方法をOnActionExecutedの内側に提供されていますので、_receivedEntityTags方法及び保管てETagは不要である()全体OnActionExecutingよく
  • のみObjectContent応答タイプで動作します。
  • ためのシリアライゼーションの余分な作業負荷。

また、それは問題の一部ではなかったし、誰もがそれを言及していません。しかし、キャッシュ検証のためにETagを使用する必要があります。したがって、Cache-Controlヘッダーとともに使用する必要があります。これにより、クライアントはキャッシュが期限切れになるまでサーバーを呼び出す必要がなくなります(リソースが非常に短いことがあります)。キャッシュが期限切れになると、クライアントはETagで要求を行い、それを検証します。キャッシングの詳細についてはsee this articleを参照してください。私は少しそれをヒモことにしましたが、なぜ

は、だからです。簡略化されたフィルタOnActionExecutingメソッドは必要ありません。Any型で動作しますが、シリアライゼーションはありません。最も重要なのは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("-", ""); 
     } 
    } 
} 

使用法例えば:1分、クライアント側のキャッシュに:

[ClientCacheWithEtag(60)]