2011-02-06 3 views
29

데이터베이스 집약적 인 인 특정 페이지가있는 ASP.NET MVC 3 (면도기) 웹 응용 프로그램이 있으며 사용자 경험이 최우선 순위입니다.MVC 패턴을 위반하지 않고 캐싱 모델을 구현하는 방법은 무엇입니까?

따라서이 특정 페이지에 캐싱을 도입하려고합니다.

은 현재 캐싱없이처럼 나는, 내 컨트롤러 얇은을 유지하는 동안이 캐싱 패턴을 구현하는 방법을 알아 내려고 노력 해요 : 당신이 볼 수 있듯이

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

는 컨트롤러가 매우 얇은 , 있어야합니다. 그것은 정보를 얻는 방법/위치에 상관하지 않습니다. 이것이 서비스의 일입니다.

제어 흐름에 대한 참고 사항 몇 :

  1. 컨트롤러는 그것의 지역에 따라 특정 서비스을 DI'ed 얻을. 이 예에서,이 컨트롤러의 GET는 IQueryable<T>저장소을 통해 LocationService
  2. 서비스 전화 그리고 T 또는 ICollection<T>에 결과를 실현.

어떻게 캐싱을 구현하려는 :

  • 내가 출력 캐싱을 사용할 수 없습니다 - 몇 가지 이유. 우선,이 액션 메소드는 클라이언트 측 (jQuery/AJAX)에서 [HttpPost]을 통해 호출되며 HTTP 표준에 따라 요청으로 캐시해서는 안됩니다. 둘째, 나는 순수하게 HTTP 요청 인수를 기반으로 캐시하고 싶지 않습니다. 캐시 논리는 훨씬 복잡합니다. 실제로 실제로는 2 단계 캐싱이 진행됩니다.
  • 위에서 언급했듯이 일반 데이터 캐싱 (예 : Cache["somekey"] = someObj;)을 사용해야합니다.
  • 모두 서비스를 통해 호출이 먼저 캐시를 통과하는 일반적인 캐싱 메커니즘을 구현하고 싶지 않습니다. 이 특정 작업 메서드에서만 캐싱을 원합니다.

먼저 생각의은 (LocationService을 상속하는) 다른 서비스를 만들 말해, 거기 캐싱 워크 플로우를 제공한다 (첫번째 캐시를 확인, DB가 호출 할 경우, 캐시, 반환 결과에 추가 할) 것입니다. 이 문제가

는 : - 여분의 아무것도에 대한 참조

  1. 서비스는 기본 클래스 라이브러리 있습니다. 여기에 System.Web에 대한 참조를 추가해야합니다.
  2. 웹 응용 프로그램 외부의 HTTP 컨텍스트에 액세스해야합니다. 이것은 테스트 가능성뿐만 아니라 일반적으로 나쁜 습관으로 간주됩니다.

는 또한 바로 소리를하지 않는 모델 폴더에 캐시 서비스를 (내가 현재 ViewModels 만 사용) 웹 응용 프로그램에 Models 폴더를 사용하지만, 문제에 대해 생각했다.

그래서 - 아이디어가 있습니까? 여기에 사용할 수있는 MVC 관련 항목 (예 : Action Filter 's)이 있습니까?

일반적인 조언/도움말을 크게 높이세요.

+0

''이 특정 액션 메소드에서만 캐싱을 원합니다. '- 당신이 ActionFilter 솔루션을 요구하는 것처럼 들립니다. – Omar

답변

6

내 대답은 서비스가 인터페이스를 구현한다고 가정합니다. 예를 들어 _locationService의 유형은 실제로 ILocationService이지만 구체적인 LocationService가 주입됩니다.ILocationService 인터페이스를 구현하는 CachingLocationService를 만들고이 캐싱 버전의 서비스를이 컨트롤러에 삽입하도록 컨테이너 구성을 변경합니다. CachingLocationService 자체는 원래 LocationService 클래스로 삽입 될 ILocationService에 종속됩니다. 이 기능을 사용하여 실제 비즈니스 로직을 실행하고 캐시에서 당기거나 푸시하는 것에 만 관심을가집니다.

원래 LocationService와 동일한 어셈블리에서 CachingLocationService를 만들 필요가 없습니다. 귀하의 웹 어셈블리에있을 수 있습니다. 그러나 개인적으로 원래의 어셈블리에 넣고 새로운 참조를 추가했습니다.

HttpContext에 대한 종속성을 추가하는 방법은 다음과 같습니다. 당신은

Func<HttpContextBase> 

에 대한 종속성을 복용하고 HttpContextBase을 조롱 할 수 있습니다 테스트에서 그런

() => HttpContext.Current 

처럼 뭔가 런타임에이 주입하여이를 제거 할 수 있습니다,하지만 당신은 문제없이 Cache 개체를 조롱이있을 수 있습니다 TypeMock과 같은 것을 사용합니다.


편집 : 더 .NET 4 System.Runtime.Caching 네임 스페이스에 독서에, 당신의 CachingLocationService가 ObjectCache에 종속성을해야합니다. 이것은 캐시 구현을위한 추상 기본 클래스입니다. 그런 다음 예를 들어 System.Runtime.Caching.MemoryCache.Default를 사용하여 주입 할 수 있습니다.

+0

흥미 롭습니다. 그리고 맞습니다, DI를 통해'LocationService'가 주입 된'ILocationService'를 사용하고 있습니다. 'HttpContext' (그리고'HttpContextBase'조차도)의 문제점은 'System.Web' 어셈블리에 존재한다는 것입니다. 그래서 내 "서비스"프로젝트는 이것을 (절대적으로) 피하려고 노력하는 참조가 필요할 것입니다. 귀하의 답변에 감사드립니다. 나는 여기에 몇 가지 대답을 가지고있다. 나는 그들을 소화 할 시간이 필요하다. :) – RPM1984

4

데이터베이스에서 가져 오는 데이터를 캐시에 저장하려는 것 같습니다..

/// <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라는 정적 클래스에서 그 방법을 가지고 : 여기이 (나는 많은 오픈 소스 MVC 프로젝트에 사용되는 본 적이 접근 방법)을 처리하는 방법입니다. 이러한 방법을 컨트롤러 액션에 적용하는 방법은 다음과 같습니다.

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

+2

@Maxim, 캐싱이 필요한 각 작업에이 코드를 작성합니까? –

+0

@whatispunk이 예에서는 SearchPreferences가 키로 사용되고 결과는 캐시 된 개체입니다. –

7

일반적인 조언을 제공하고 올바른 방향을 알려줍니다.

  1. 응용 프로그램에서 캐싱을 처음 시도하는 경우 HTTP 응답을 캐싱하지 말고 대신 응용 프로그램 데이터를 캐시하십시오. 일반적으로 데이터를 캐싱하고 데이터베이스에 호흡 실을 제공하기 시작합니다. 앱이 충분하지 않고 앱/웹 서버에 큰 스트레스가있는 경우 HTTP 응답 캐싱을 생각해 볼 수 있습니다.

  2. MVC 패러다임의 다른 모델로 데이터 캐시 계층을 모든 후속 함의로 처리하십시오.

  3. 무엇을 하든지 자신의 캐시를 작성하지 마십시오. 항상 실제보다 더 쉽게 보입니다. memcached와 같은 것을 사용하십시오.

+0

팁 주셔서 감사합니다. 내가 따라야 할 모든 유효한 포인트. – RPM1984

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을 제공합니다.

+3

굉장합니다. 그냥 (언제나처럼) 대단합니다. 네,'System.Runtime.Caching'에 대해 들었는데, 그것이 새로운/.NET 4 일이라는 것을 몰랐습니다. ORM에 관해서는 - 이것은 나중에 저장소 (저장소의 서비스 호출 메소드)에 있고 EF4를 사용하는 인스턴트 메신저입니다. 나는 "기본 객체"를 캐시하고 싶지 않다. 나는 커스텀 객체를 캐싱하고있다. 그러나 액션 필터는 좋은 생각처럼 보입니다. (지금까지 제대로 구현하는 법을 알지 못했습니다.) - 내가 플레이 할 겁니다. – RPM1984

+0

Hahahaha @dislaimer edit. 친구를 마셔라. 마땅히 받아 들여야한다. 걱정하지 마라. 아무도 MVC "Top Users"목록에서 탈퇴하지 않을 것이다. :) – RPM1984

+0

ActionFilter의 탁월한 사용법. – adammokan

4

조쉬의 대답 @ 받아,하지만 난 정확히 그는 (닫기) 무엇을 제안과 함께 할 것입니다, 그래서 완전성에 대한 생각 내가 내가 실제로 추가 거라고하지 않았기 때문에 나는 내 자신의 대답을 추가 할 거라고 생각했습니다 그랬어.

열쇠는 현재 System.Runtime.Caching입니다. NET 특정 및 ASP.NET 특정 아닌 어셈블리에 존재하기 때문에이 내 서비스를 참조하는 데 문제가 있습니다.

그래서 내가 한 모든 것은 캐싱을 필요로하는 특정 서비스 레이어 방법에 캐싱 로직을 넣는 것뿐입니다.

그리고 중요한 점은, System.Runtime.Caching.ObjectCache 클래스를 처리하는 것입니다. 이것은 서비스의 생성자에 주입됩니다.

현재 DI가 System.Runtime.Caching.MemoryCache 개체를 주입합니다. ObjectCache 클래스에 대한 좋은 점은 그것이 추상적이며 모든 핵심 메소드가 가상이라는 것입니다.

내 단위 테스트에서는 모든 메서드를 재정의하고 기본 캐시 메커니즘을 구현하는 간단한 Dictionary<TKey,TValue>을 구현하여 MockCache 클래스를 만들었 음을 의미합니다.

Velocity으로 곧 전환 할 예정입니다. 다시 말해서, 나는 또 다른 ObjectCache 파생 클래스를 생성해야하며 갈 수 있습니다.

도움을 주신 모든 분들께 감사드립니다.

관련 문제