2014-11-06 52 views
1

編輯:對於每個請求,創建一個新的控制器實例。但是,對於Attribute類,這不是真的。一旦它們被創建,它就被用於多個請求。我希望它有幫助。緩存WebAPI 2

我寫我自己的WebAPI(使用最新版本的WebAPI和.NET Framework)的緩存行爲過濾器。我知道CacheCow & this。不過,我想我的反正。

然而,有一些問題,我的代碼,因爲當我在現場服務器項目中使用它我不明白exepected輸出。在本地機器上一切正常。

我用下面的代碼在我的博客RSS生成和我緩存數據,爲每個類別。有大約5類(食品,科技,個人等)。

問題:當我導航說api/GetTech時,它會返回我個人博客類別中的RSS源項目。當我瀏覽說API/GetPersonal,它返回我的API /食品

我無法找到問題的根源,但我認爲這是由於使用了靜態方法/變量。我再一次檢查了我的_cachekey對我的博客的每個類別都有獨特的價值。

可有人指出,此代碼ESP任何問題,當我們說每分鐘300個請求?

public class WebApiOutputCacheAttribute : ActionFilterAttribute 
    { 
     // Cache timespan 
     private readonly int _timespan; 

     // cache key 
     private string _cachekey; 

     // cache repository 
     private static readonly MemoryCache _webApiCache = MemoryCache.Default; 
     /// <summary> 
     /// Initializes a new instance of the <see cref="WebApiOutputCacheAttribute"/> class. 
     /// </summary> 
     /// <param name="timespan">The timespan in seconds.</param> 
     public WebApiOutputCacheAttribute(int timespan) 
     { 
      _timespan = timespan; 
     } 

     public override void OnActionExecuting(HttpActionContext ac) 
     { 
      if (ac != null) 
      { 
       _cachekey = ac.Request.RequestUri.PathAndQuery.ToUpperInvariant(); 

       if (!_webApiCache.Contains(_cachekey)) return; 

       var val = (string)_webApiCache.Get(_cachekey); 

       if (val == null) return; 

       ac.Response = ac.Request.CreateResponse(); 
       ac.Response.Content = new StringContent(val); 
       var contenttype = (MediaTypeHeaderValue)_webApiCache.Get("response-ct") ?? new MediaTypeHeaderValue("application/rss+xml"); 
       ac.Response.Content.Headers.ContentType = contenttype; 
      } 
      else 
      { 
       throw new ArgumentNullException("ac"); 
      } 
     } 




     public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) 
     { 
      if (_webApiCache.Contains(_cachekey)) return; 
      var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result; 
      if (actionExecutedContext.Response.StatusCode == HttpStatusCode.OK) 
      { 
       lock (WebApiCache) 
       { 
        _wbApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan)); 
        _webApiCache.Add("response-ct", actionExecutedContext.Response.Content.Headers.ContentType, DateTimeOffset.UtcNow.AddSeconds(_timespan)); 
       } 

      } 

     } 



    } 

+0

「我認爲當一個類有私有變量&說我們有2個類的實例時,每個類都有它自己的內存和自己的私有變量。」 - 是的,我誤導了你。但是,在這種情況下你並沒有處理2個實例。您正在處理2個或更多不同請求線程正在使用的1個實例。 – danludwig 2014-11-06 14:15:39

+0

是的。我認爲我不需要鎖定代碼,因爲MemoryCache是​​線程安全的。我的理解是正確的嗎? – NoobDeveloper 2014-11-06 16:29:47

+0

將值添加到緩存時,「鎖定」是不必要的,只會減慢使用該屬性的請求,但可能不會太多。 – danludwig 2014-11-07 14:03:03

回答

2

同樣WebApiOutputCacheAttribute實例可以用於緩存多個同時請求,所以你不應該在該屬性的實例存儲緩存鍵。而是在每個請求/方法覆蓋期間重新生成緩存鍵。以下屬性用於緩存HTTP GET請求。

using System; 
using System.Linq; 
using System.Net.Http; 
using System.Net.Http.Headers; 
using System.Web.Http.Controllers; 
using System.Web.Http.Filters; 
using Newtonsoft.Json; 

// based on strathweb implementation 
// http://www.strathweb.com/2012/05/output-caching-in-asp-net-web-api/ 
public class CacheHttpGetAttribute : ActionFilterAttribute 
{ 
    public int Duration { get; set; } 

    public ILogExceptions ExceptionLogger { get; set; } 
    public IProvideCache CacheProvider { get; set; } 

    private bool IsCacheable(HttpRequestMessage request) 
    { 
     if (Duration < 1) 
      throw new InvalidOperationException("Duration must be greater than zero."); 

     // only cache for GET requests 
     return request.Method == HttpMethod.Get; 
    } 

    private CacheControlHeaderValue SetClientCache() 
    { 
     var cachecontrol = new CacheControlHeaderValue 
     { 
      MaxAge = TimeSpan.FromSeconds(Duration), 
      MustRevalidate = true, 
     }; 
     return cachecontrol; 
    } 

    private static string GetServerCacheKey(HttpRequestMessage request) 
    { 
     var acceptHeaders = request.Headers.Accept; 
     var acceptHeader = acceptHeaders.Any() ? acceptHeaders.First().ToString() : "*/*"; 
     return string.Join(":", new[] 
     { 
      request.RequestUri.AbsoluteUri, 
      acceptHeader, 
     }); 
    } 

    private static string GetClientCacheKey(string serverCacheKey) 
    { 
     return string.Join(":", new[] 
     { 
      serverCacheKey, 
      "response-content-type", 
     }); 
    } 

    public override void OnActionExecuting(HttpActionContext actionContext) 
    { 
     if (actionContext == null) throw new ArgumentNullException("actionContext"); 
     var request = actionContext.Request; 
     if (!IsCacheable(request)) return; 

     try 
     { 
      // do NOT store cache keys on this attribute because the same instance 
      // can be reused for multiple requests 
      var serverCacheKey = GetServerCacheKey(request); 
      var clientCacheKey = GetClientCacheKey(serverCacheKey); 

      if (CacheProvider.Contains(serverCacheKey)) 
      { 
       var serverValue = CacheProvider.Get(serverCacheKey); 
       var clientValue = CacheProvider.Get(clientCacheKey); 
       if (serverValue == null) return; 

       var contentType = clientValue != null 
        ? JsonConvert.DeserializeObject<MediaTypeHeaderValue>(clientValue.ToString()) 
        : new MediaTypeHeaderValue(serverCacheKey.Substring(serverCacheKey.LastIndexOf(':') + 1)); 

       actionContext.Response = actionContext.Request.CreateResponse(); 

       // do not try to create a string content if the value is binary 
       actionContext.Response.Content = serverValue is byte[] 
        ? new ByteArrayContent((byte[])serverValue) 
        : new StringContent(serverValue.ToString()); 

       actionContext.Response.Content.Headers.ContentType = contentType; 
       actionContext.Response.Headers.CacheControl = SetClientCache(); 
      } 
     } 
     catch (Exception ex) 
     { 
      ExceptionLogger.Log(ex); 
     } 
    } 

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) 
    { 
     try 
     { 
      var request = actionExecutedContext.Request; 

      // do NOT store cache keys on this attribute because the same instance 
      // can be reused for multiple requests 
      var serverCacheKey = GetServerCacheKey(request); 
      var clientCacheKey = GetClientCacheKey(serverCacheKey); 
      if (!CacheProvider.Contains(serverCacheKey)) 
      { 
       var contentType = actionExecutedContext.Response.Content.Headers.ContentType; 
       object serverValue; 
       if (contentType.MediaType.StartsWith("image/")) 
        serverValue = actionExecutedContext.Response.Content.ReadAsByteArrayAsync().Result; 
       else 
        serverValue = actionExecutedContext.Response.Content.ReadAsStringAsync().Result; 
       var clientValue = JsonConvert.SerializeObject(
        new 
        { 
         contentType.MediaType, 
         contentType.CharSet, 
        }); 
       CacheProvider.Add(serverCacheKey, serverValue, new TimeSpan(0, 0, Duration)); 
       CacheProvider.Add(clientCacheKey, clientValue, new TimeSpan(0, 0, Duration)); 
      } 

      if (IsCacheable(actionExecutedContext.Request)) 
       actionExecutedContext.ActionContext.Response.Headers.CacheControl = SetClientCache(); 
     } 
     catch (Exception ex) 
     { 
      ExceptionLogger.Log(ex); 
     } 
    } 
} 

只需用您的MemoryCache.Default替換CacheProvider即可。實際上,上面的代碼在開發過程中默認使用相同的,並且在部署到活動服務器時使用azure緩存。

即使你的代碼中的每個請求期間重置_cachekey實例字段,這些屬性是不喜歡在那裏爲每個請求創建一個新的控制器。相反,可以將屬性實例重用爲服務多個同時請求。因此,不要使用實例字段來存儲它,每次需要時都會根據請求重新生成它。

+0

我不明白。假設我有一個「TechCategory」關鍵字的緩存項,那麼對於下一個請求,我只需檢查MemoryCache對象,看看我是否已經擁有了關鍵「TechCategory」的數據。如果存在,則返回緩存的輸出。我的代碼有什麼問題? – NoobDeveloper 2014-11-06 13:38:27

+0

問題是'_cacheKey'可以由請求A設置,然後被請求B重用,因爲一個屬性實例可以用來服務多個同時發生的請求。而不是設置此字段並重新使用它,請每次需要使用它時根據請求重新生成該字段。 – danludwig 2014-11-06 13:40:53

+0

我明白你的意思,但有一些困惑。所以我的代碼中的_cacheKey是根據每個請求重置? – NoobDeveloper 2014-11-06 13:43:14