2011-02-06 155 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派生类,我很好去。

感谢大家的帮助!

相关问题