2011-02-06 156 views
29

我有一個ASP.NET MVC 3(Razor)Web應用程序,其中一個特定頁面是高度數據庫密集型,用戶體驗是最重要的。如何在不違反MVC模式的情況下實現緩存模型?

因此,我在這個特定的頁面上引入了緩存。

我試圖想出一個辦法來實現這個緩存模式,同時保持我的控制器,喜歡它目前是沒有緩存:

public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences) 
{ 
    var results = _locationService.FindStuffByCriteria(searchPreferences); 
    return PartialView("SearchResults", results); 
} 

正如你所看到的,控制器是非常薄,因爲它應該是。它不關心如何/從哪裏獲取信息 - 這是服務的工作。

上控制流有兩點要注意:

  1. 控制器得到DI'ed特定服務,這取決於它的面積。在這個例子中,該控制器GET是一個LocationService
  2. 服務電話接到一個IQueryable<T>和物化成果轉化爲TICollection<T>

如何我想實現緩存:

  • 我不能使用輸出緩存 - 有幾個原因。首先,這個動作方法是通過[HttpPost]從客戶端(jQuery/AJAX)調用的,它根據HTTP標準不應該被緩存爲請求。其次,我不想純粹基於HTTP請求參數進行緩存 - 緩存邏輯比這更復雜 - 實際上存在兩級緩存。
  • 正如我上面提到的,我需要使用常規數據緩存,例如Cache["somekey"] = someObj;
  • 我不想實現一個通用的緩存機制,其中全部通過服務調用首先通過緩存 - 我只想緩存這個特定的動作方法

首先想到的會告訴我創建另一個服務(它繼承LocationService),並提供高速緩存的工作流程有(先檢查高速緩存,如果不是有叫分貝,增加緩存,返回結果)。

有兩個問題:

  1. 的服務是基本類庫 - 對任何額外的引用。我需要在這裏添加對System.Web的引用。
  2. 我將不得不訪問Web應用程序之外的HTTP上下文,這被認爲是不好的做法,不僅是爲了可測試性,而是在一般情況下 - 對嗎?

我也想過使用Models文件夾中的Web應用程序(這是我目前只使用的ViewModels),但有一個緩存服務的模型文件夾只是不健全的權利。

那麼 - 任何想法?是否有MVC特定的東西(例如Action Filter的),我可以在這裏使用?

一般諮詢/提示將不勝感激。

+0

``我只想緩存在這個特定的動作方法上`` - 聽起來像是在尋求一個ActionFilter解決方案。 – Omar 2011-02-06 23:18:45

回答

6

我的回答是基於你的服務實現的接口,例如_locationService的類型實際上是ILocationService但被注入混凝土LocationService的假設。創建一個實現ILocationService接口的CachingLocationService,並更改容器配置以將該服務的緩存版本注入到此控制器。 CachingLocationService本身對ILocationService有一個依賴關係,它將與原始的LocationService類一起注入。它會使用它來執行真正的業務邏輯,並只關心從緩存中拉取和推送。

您不需要在與原始LocationService相同的程序集中創建CachingLocationService。它可能在你的web程序集中。但是,我個人將它放在原始程序集中並添加新的參考。

至於添加對HttpContext的依賴關係;你可以通過在

Func<HttpContextBase> 

的依賴,並在運行時用類似

() => HttpContext.Current 

然後在您的測試,你可以嘲笑HttpContextBase注入這個刪除,但你可能會遇到麻煩嘲諷Cache對象,而不使用類似TypeMock的東西。


編輯:在基於.NET 4 System.Runtime.Caching命名空間進一步閱讀,你的CachingLocationService應該採取ObjectCache的依賴。這是緩存實現的抽象基類。例如,您可以使用System.Runtime.Caching.MemoryCache.Default注入它。

4

這聽起來像你想緩存你從數據庫獲得的數據。下面是我如何處理這個(我已經在很多開源項目的MVC使用見過的方法):

/// <summary> 
    /// remove a cached object from the HttpRuntime.Cache 
    /// </summary> 
    public static void RemoveCachedObject(string key) 
    { 
     HttpRuntime.Cache.Remove(key); 
    } 

    /// <summary> 
    /// retrieve an object from the HttpRuntime.Cache 
    /// </summary> 
    public static object GetCachedObject(string key) 
    { 
     return HttpRuntime.Cache[key]; 
    } 

    /// <summary> 
    /// add an object to the HttpRuntime.Cache with an absolute expiration time 
    /// </summary> 
    public static void SetCachedObject(string key, object o, int durationSecs) 
    { 
     HttpRuntime.Cache.Add(
      key, 
      o, 
      null, 
      DateTime.Now.AddSeconds(durationSecs), 
      Cache.NoSlidingExpiration, 
      CacheItemPriority.High, 
      null); 
    } 

    /// <summary> 
    /// add an object to the HttpRuntime.Cache with a sliding expiration time. sliding means the expiration timer is reset each time the object is accessed, so it expires 20 minutes, for example, after it is last accessed. 
    /// </summary> 
    public static void SetCachedObjectSliding(string key, object o, int slidingSecs) 
    { 
     HttpRuntime.Cache.Add(
      key, 
      o, 
      null, 
      Cache.NoAbsoluteExpiration, 
      new TimeSpan(0, 0, slidingSecs), 
      CacheItemPriority.High, 
      null); 
    } 

    /// <summary> 
    /// add a non-removable, non-expiring object to the HttpRuntime.Cache 
    /// </summary> 
    public static void SetCachedObjectPermanent(string key, object o) 
    { 
     HttpRuntime.Cache.Remove(key); 
     HttpRuntime.Cache.Add(
      key, 
      o, 
      null, 
      Cache.NoAbsoluteExpiration, 
      Cache.NoSlidingExpiration, 
      CacheItemPriority.NotRemovable, 
      null); 
    } 

我有一個名爲Current.cs靜態類的方法。這裏是你如何運用這些方法,您的控制器動作:

public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences) 
{ 
    var prefs = (object)searchPreferences; 
    var cachedObject = Current.GetCachedObject(prefs); // check cache 
    if(cachedObject != null) return PartialView("SearchResults", cachedObject); 

    var results = _locationService.FindStuffByCriteria(searchPreferences); 
    Current.SetCachedObject(prefs, results, 60); // add to cache for 60 seconds 

    return PartialView("SearchResults", results); 
} 
+0

Current.GetCachedObject(prefs)如何工作?此方法採用緩存鍵,而不是緩存對象。 – mikesigs 2011-02-06 23:09:28

+2

@Maxim,並且您在需要緩存的每個操作中編寫此代碼? – 2011-02-06 23:10:14

+0

@whatispunk在我的示例中,SearchPreferences用作鍵,結果是緩存的對象。 – 2011-02-06 23:12:28

7

我將提供一般性諮詢,希望他們將指向您正確的方向。

  1. 如果這是你在你的應用程序緩存第一個嘗試,那麼不要緩存HTTP響應,緩存應用程序數據,而不是。通常,您首先緩存數據併爲數據庫提供一些呼吸空間;那麼,如果它不夠,你的app/web服務器承受巨大的壓力,你可以考慮緩存HTTP響應。

  2. 對待你的數據緩存層中的MVC模式的另一個模型的所有後續影響。

  3. 不管你做什麼,都不要編寫自己的緩存。它總是看起來比實際更容易。使用類似memcached的東西。

25

動作屬性似乎是一個很好的實現方法。下面是一個例子(免責聲明:我從我的頭頂寫這個:我已經消耗的啤酒一定量的寫入時此所以一定要測試它廣泛:-)):

public class CacheModelAttribute : ActionFilterAttribute 
{ 
    private readonly string[] _paramNames; 
    public CacheModelAttribute(params string[] paramNames) 
    { 
     // The request parameter names that will be used 
     // to constitute the cache key. 
     _paramNames = paramNames; 
    } 

    public override void OnActionExecuting(ActionExecutingContext filterContext) 
    { 
     base.OnActionExecuting(filterContext); 
     var cache = filterContext.HttpContext.Cache; 
     var model = cache[GetCacheKey(filterContext.HttpContext)]; 
     if (model != null) 
     { 
      // If the cache contains a model, fetch this model 
      // from the cache and short-circuit the execution of the action 
      // to avoid hitting the repository 
      var result = new ViewResult 
      { 
       ViewData = new ViewDataDictionary(model) 
      }; 
      filterContext.Result = result; 
     } 
    } 

    public override void OnResultExecuted(ResultExecutedContext filterContext) 
    { 
     base.OnResultExecuted(filterContext); 
     var result = filterContext.Result as ViewResultBase; 
     var cacheKey = GetCacheKey(filterContext.HttpContext); 
     var cache = filterContext.HttpContext.Cache; 
     if (result != null && result.Model != null && cache[key] == null) 
     { 
      // If the action returned some model, 
      // store this model into the cache 
      cache[key] = result.Model; 
     } 
    } 

    private string GetCacheKey(HttpContextBase context) 
    { 
     // Use the request values of the parameter names passed 
     // in the attribute to calculate the cache key. 
     // This function could be adapted based on the requirements. 
     return string.Join(
      "_", 
      (_paramNames ?? Enumerable.Empty<string>()) 
       .Select(pn => (context.Request[pn] ?? string.Empty).ToString()) 
       .ToArray() 
     ); 
    } 
} 

然後您的控制器動作看起來是這樣的:

[CacheModel("id", "name")] 
public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences) 
{ 
    var results = _locationService.FindStuffByCriteria(searchPreferences); 
    return View(results); 
} 

並儘可能與在業務層引用System.Web組裝你的問題而言,這不再是在.NET 4.0中的一個問題。有一個全新的程序集提供可擴展的緩存功能:System.Runtime.Caching,因此您可以使用它在您的服務層直接實現緩存。

或者即使你使用的是ORM爲您服務層大概是這樣的ORM提供了緩存功能更好?我希望它。例如NHibernate提供了一個second level cache

4

我接受了@喬希的回答,但我以爲我會加我自己的答案,因爲我沒有正好去與他建議(關閉),所以想到完整性,我會添加我實際上沒有。

關鍵是我現在使用System.Runtime.Caching。因爲它存在於.NET特定的程序集中,而不是ASP.NET特定的程序集,所以在我的服務中引用此內容時沒有任何問題。

所以我所做的就是將緩存邏輯放在需要緩存的特定服務層方法中。

還有一個重要的一點,即時工作System.Runtime.Caching.ObjectCache類 - 這是什麼get的注入服務的構造函數。

我目前的DI注入一個System.Runtime.Caching.MemoryCache對象。關於ObjectCache類的好處是它是抽象的,所有的核心方法都是虛擬的。

這意味着對於我的單元測試,我創建了一個MockCache類,覆蓋所有方法並使用簡單的Dictionary<TKey,TValue>實現底層緩存機制。

我們計劃很快切換到Velocity - 所以我需要做的就是創建另一個ObjectCache派生類,我很好去。

感謝大家的幫助!

相關問題