29 votes

Comment implémenter un modèle de mise en cache sans violer le modèle MVC?

J'ai un ASP.NET MVC 3 (Rasoir) de l'Application Web, avec une page en particulier qui est très de base de données intensive, et l'expérience de l'utilisateur est de la plus grande priorité.

Ainsi, je suis l'introduction de la mise en cache sur cette page en particulier.

J'essaie de trouver un moyen de mettre en œuvre cette mise en cache modèle tout en gardant mon contrôleur mince, comme c'est le cas actuellement, sans mise en cache:

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

Comme vous pouvez le voir, le contrôleur est très mince, comme il devrait l'être. Il ne se soucie pas comment/où il est arriver c'est de l'info de - c'est le travail du service.

Quelques notes sur le flux de contrôle:

  1. Contrôleurs obtenir DI ed un particulier de Service, en fonction de la zone. Dans cet exemple, le contrôleur est un LocationService
  2. Les Services d' appel grâce à un IQueryable<T> de Référentiel et d'en matérialiser les résultats en T ou ICollection<T>.

Comment je veux mettre en œuvre la mise en cache:

  • Je ne peux pas utiliser la mise en Cache de Sortie - pour quelques raisons. Tout d'abord, cette méthode d'action est appelée à partir du côté client (jQuery/AJAX), via [HttpPost], qui, selon les normes HTTP devraient pas être mis en cache comme une demande. Deuxièmement, je ne veux pas de cache purement basé sur la requête HTTP arguments - le cache logique est beaucoup plus compliqué que cela - il n'y a en fait deux niveaux de mise en cache se passe.
  • Comme je l'astuce ci-dessus, j'ai besoin de l'utilisation régulière des données mise en cache, l'e.g Cache["somekey"] = someObj;.
  • Je ne veux pas mettre en œuvre un générique mécanisme de mise en cache où tous les appels via le service passent par le cache en premier - je veux seulement la mise en cache sur cette méthode d'action.

La première pensée est de me créer un autre service (qui hérite LocationService), et de fournir la mise en cache des flux de travail, il (à vérifier cache en premier, si il n'y a pas d'appel db, ajouter de cache, de retour de résultat).

Qui a deux problèmes:

  1. Les services sont à la base des Bibliothèques de Classe - ne pas faire référence à quelque chose de supplémentaire. J'aurais besoin d'ajouter une référence à l' System.Web ici.
  2. Je voudrais avoir accès au Contexte HTTP dehors de l'application web, qui est considéré comme une mauvaise pratique, non seulement pour la testabilité, mais en général - droit?

J'ai également pensé à l'aide de l' Models le dossier de l'Application Web (que j'utilise actuellement seulement pour Viewmodel), mais avoir un service de cache dans un dossier de modèles n'a tout simplement pas l'air bon.

Donc, - des idées? Est-il un MVC-chose spécifique (comme l'Action du Filtre, par exemple) je peux utiliser ici?

Des conseils généraux ou des conseils seraient grandement appréciés.

25voto

Darin Dimitrov Points 528142

Un attribut d'action semble être un bon moyen d'y parvenir. Voici un exemple (disclaimer: je suis en train d'écrire ce à partir du haut de ma tête: j'ai consommé une certaine quantité de bière lors de l'écriture de ce alors assurez-vous de tester de manière approfondie :-)):

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

Et puis, votre contrôleur de l'action pourrait ressembler à ceci:

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

Et en ce qui concerne votre problème avec le référencement System.Web de l'assemblée de la couche service est concerné, ce n'est plus un problème .NET 4.0. Il y a une complètement nouvelle assemblée qui fournit extensible fonctionnalités de mise en cache : Système.Moment de l'exécution.La mise en cache, de sorte que vous pouvez l'utiliser pour mettre en œuvre la mise en cache dans votre couche de service directement.

Ou encore mieux si vous utilisez un ORM à votre service couche probablement ce ORM fournit des capacités de mise en cache? J'espère qu'il ne. Par exemple NHibernate fournit un cache de second niveau.

7voto

Yuriy Zubarev Points 1888

Je vais vous donner de conseils généraux et de, j'espère qu'ils pointent vers la bonne direction.

  1. Si c'est votre premier coup de couteau à la mise en cache dans votre application, alors ne le faites pas la mise en cache de la réponse HTTP, cache les données de l'application à la place. Habituellement, vous commencez avec la mise en cache des données et de donner à votre base de données de respirer; puis, si c'est pas assez et votre app/serveurs web sont sous stress énorme, vous pouvez penser de la mise en cache des réponses HTTP.

  2. Traiter vos données de cache couche comme un autre Modèle MVC paradigme avec toutes les implications.

  3. Quoi que vous fassiez, ne pas écrire votre propre cache. Elle a toujours l'air plus facile qu'il ne l'est vraiment. Utilisez quelque chose comme memcached.

6voto

Josh Gallagher Points 1591

Ma réponse est basée sur l'hypothèse que vos services implémenter une interface, par exemple le type de _locationService est en fait ILocationService mais est injecté avec un béton LocationService. Créer un CachingLocationService qui implémente l'ILocationService interface et changer votre configuration de conteneur pour injecter de la mise en cache de la version du service à ce contrôleur. Le CachingLocationService aurait elle-même une dépendance au sur ILocationService qui serait injecté avec l'original LocationService classe. Il serait utiliser pour exécuter le véritable logique d'entreprise et de s'occuper que de tirer et de pousser à partir du cache.

Vous n'avez pas besoin de créer CachingLocationService dans la même assemblée que l'original LocationService. Il pourrait être dans votre site web de l'assemblée. Cependant, personnellement, je l'avais mis à l'origine, de l'assemblée et ajouter la nouvelle référence.

Comme pour l'ajout d'une dépendance sur HttpContext; vous pouvez le supprimer en prenant une dépendance sur

Func<HttpContextBase> 

et d'injecter de l'air lors de l'exécution avec quelque chose comme

() => HttpContext.Current

Alors dans vos tests, vous pouvez vous moquer de HttpContextBase, mais vous pouvez avoir de la difficulté à se moquer de la Cache de l'objet sans l'aide de quelque chose comme TypeMock.


Edit: Sur la poursuite de la lecture sur le .NET 4 Système.Moment de l'exécution.La mise en cache de noms, votre CachingLocationService devrait prendre une dépendance sur ObjectCache. C'est la classe de base abstraite pour les implémentations de cache. Vous pouvez ensuite injecter qu'avec le Système.Moment de l'exécution.La mise en cache.MemoryCache.Par défaut, par exemple.

4voto

Maxim Zaslavsky Points 6873

Il semble que vous essayez de mettre en cache les données que vous obtenez de votre base de données . Voici comment je gère cela (une approche que j'ai vue utilisée dans de nombreux projets MVC open source):

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

J'ai ces méthodes dans une classe statique nommée Current.cs . Voici comment appliquer ces méthodes à l'action de votre contrôleur:

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

4voto

RPM1984 Points 39648

J'ai accepté @Josh réponse, mais j'ai pensé ajouter ma propre réponse, parce que je n'ai pas exactement ce qu'il suggère (à proximité), donc la pensée de l'exhaustivité j'ajouterais ce que j'ai fait.

La clé est, je suis maintenant en utilisant System.Runtime.Caching. Parce que cela existe en une assemblée qui est .NET et non à l'ASP.NET spécifique, je n'ai pas de problèmes de référencement, c'est à mon service.

Donc, tout ce que j'ai fait, c'est mettre en cache dans la logique spécifique de la couche de service méthodes qui nécessitent la mise en cache.

Et un point important, im travaillant hors d' System.Runtime.Caching.ObjectCache classe - c'est ce qui est injecté dans le constructeur du service.

Ma DI injecte un System.Runtime.Caching.MemoryCache objet. La bonne chose à propos de l' ObjectCache classe, c'est qu'il est abstrait et toutes les méthodes sont virtuelles.

Ce qui signifie que pour mes tests d'unité, j'ai créé un MockCache classe, en remplaçant toutes les méthodes et la mise en œuvre de la sous-jacentes mécanisme de cache avec un simple Dictionary<TKey,TValue>.

Nous avons l'intention de changer de Vitesse rapidement - donc encore une fois, je n'ai besoin de faire est de créer un autre ObjectCache dérivant de la classe et je suis bon pour aller.

Merci pour l'aide tout le monde!

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X