Comment mettre en cache des données dans une application MVC

252

J'ai lu beaucoup d'informations sur la mise en cache des pages et la mise en cache des pages partielle dans une application MVC. Cependant, j'aimerais savoir comment vous mettriez en cache les données.

Dans mon scénario, j'utiliserai LINQ to Entities (framework d'entité). Lors du premier appel à GetNames (ou quelle que soit la méthode), je veux récupérer les données de la base de données. Je veux enregistrer les résultats dans le cache et au deuxième appel pour utiliser la version mise en cache si elle existe.

Quelqu'un peut-il montrer un exemple de la façon dont cela fonctionnerait, où cela devrait être mis en œuvre (modèle?) Et si cela fonctionnerait.

J'ai vu cela dans des applications ASP.NET traditionnelles, généralement pour des données très statiques.

Coolcoder
la source
1
En examinant les réponses ci-dessous, assurez-vous de savoir si vous souhaitez que votre contrôleur connaisse / soit responsable des problèmes d'accès aux données et de mise en cache. En général, vous voulez séparer cela. Voir le modèle de référentiel pour une bonne façon de le faire: deviq.com/repository-pattern
ssmith

Réponses:

75

Référencez la DLL System.Web dans votre modèle et utilisez System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

Un peu simplifié mais je suppose que ça marcherait. Ce n'est pas spécifique à MVC et j'ai toujours utilisé cette méthode pour mettre les données en cache.

terjetyl
la source
89
Je ne recommande pas cette solution: dans le retour, vous pourriez obtenir à nouveau un objet nul, car il relit dans le cache et il a peut-être déjà été supprimé du cache. Je préfère faire: public string [] GetNames () {string [] noms = Cache ["names"]; if (noms == null) {noms = DB.GetNames (); Cache ["names"] = noms; } return (noms); }
Oli
Je suis d'accord avec Oli .. obtenir les résultats de l'appel réel vers la base de données est mieux que de les obtenir du cache
CodeClimber
1
Est-ce que cela fonctionne avec la DB.GetNames().AsQueryableméthode de retard de la requête?
Chase Florell
Sauf si vous changez la valeur de retour de la chaîne [] en IEnumerable <string>
terjetyl
12
Si vous ne définissez pas d'expiration ... quand le cache expire-t-il par défaut?
Chaka
403

Voici une classe / service d'aide au cache agréable et simple que j'utilise:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

Usage:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

Le fournisseur de cache vérifiera s'il y a quelque chose par le nom de "id de cache" dans le cache, et s'il n'y en a pas, il appellera une méthode déléguée pour récupérer les données et les stocker dans le cache.

Exemple:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())
Hrvoje Hudo
la source
3
J'ai adapté cela pour que le mécanisme de mise en cache soit utilisé par session utilisateur en utilisant HttpContext.Current.Session à la place. J'ai également mis une propriété Cache sur ma classe BaseController afin que son accès facile et la mise à jour du constructeur permettent la DI pour les tests unitaires. J'espère que cela t'aides.
WestDiscGolf
1
Vous pouvez également rendre cette classe et cette méthode statiques pour les réutiliser parmi d'autres contrôleurs.
Alex
5
Cette classe ne doit pas dépendre de HttpContext. Je l'ai simplifié juste à titre d'exemple ici. L'objet cache doit être inséré via le constructeur - il peut ensuite être remplacé par d'autres mécanismes de mise en cache. Tout cela est réalisé avec IoC / DI, avec un cycle de vie statique (singleton).
Hrvoje Hudo
3
@Brendan - et pire encore, il a des chaînes magiques en place pour les clés de cache, plutôt que de les déduire du nom de la méthode et des paramètres.
ssmith
5
Ceci est une solution de bas niveau impressionnante. Comme d'autres l'ont fait allusion, vous voudriez envelopper cela dans une classe sécurisée de type spécifique au domaine. Accéder à cela directement dans vos contrôleurs serait un cauchemar de maintenance en raison des chaînes magiques.
Josh Noe
43

Je fais référence au post de TT et suggère l'approche suivante:

Référencez la DLL System.Web dans votre modèle et utilisez System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

Vous ne devez pas retourner une valeur relue depuis le cache, car vous ne saurez jamais si à ce moment précis, elle est toujours dans le cache. Même si vous l'avez inséré dans la déclaration précédente, il est peut-être déjà parti ou n'a jamais été ajouté au cache - vous ne le savez tout simplement pas.

Vous ajoutez donc les données lues à partir de la base de données et les renvoyez directement, sans relire à partir du cache.

Oli
la source
Mais la ligne n'est-elle pas Cache["names"] = noms;mise dans le cache?
Omar
2
@Baddie Oui, c'est vrai. Mais cet exemple est différent du premier auquel Oli fait référence, car il n'accède plus au cache - le problème est qu'il suffit de faire: return (string []) Cache ["names"]; .. POURRAIT entraîner le retour d'une valeur nulle, car elle POURRAIT expirer. Ce n'est pas probable, mais cela peut arriver. Cet exemple est meilleur, car nous stockons la valeur réelle renvoyée par la base de données en mémoire, mettons en cache cette valeur, puis renvoyons cette valeur, et non la valeur relue à partir du cache.
jamiebarrow
Ou ... la valeur relue à partir du cache, si elle existe toujours (! = Null). Par conséquent, le point entier de mise en cache. C'est juste pour dire qu'il vérifie les valeurs nulles et lit la base de données si nécessaire. Très intelligent, merci Oli!
Sean Kendle
Pouvez-vous s'il vous plaît partager un lien où je peux lire sur la mise en cache des applications basée sur les valeurs clés. Je ne trouve pas de liens.
Incassable
@Oli, Comment consommer ces enregistrements de cache à partir d'une page CSHTML ou HTML
Deepan Raj
37

Pour le framework .NET 4.5+

ajouter une référence: System.Runtime.Caching

ajouter en utilisant l'instruction: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

Dans le .NET Framework 3.5 et les versions antérieures, ASP.NET a fourni une implémentation de cache en mémoire dans l'espace de noms System.Web.Caching. Dans les versions précédentes de .NET Framework, la mise en cache n'était disponible que dans l'espace de noms System.Web et nécessitait donc une dépendance aux classes ASP.NET. Dans le .NET Framework 4, l'espace de noms System.Runtime.Caching contient des API conçues pour les applications Web et non Web.

Plus d'informations:

juFo
la source
D'où Db.GerNames()vient-il?
Junior
DB.GetNames est juste une méthode du DAL qui récupère certains noms de la base de données. C'est tout ce que vous récupérez normalement.
juFo
Cela devrait être au sommet car il a la solution pertinente actuelle
BYISHIMO Audace
2
Merci, il fallait également ajouter le package de nuget System.Runtime.Caching (v4.5).
Steve Greene
26

Steve Smith a fait deux excellents articles de blog qui montrent comment utiliser son modèle CachedRepository dans ASP.NET MVC. Il utilise efficacement le modèle de référentiel et vous permet d'obtenir la mise en cache sans avoir à modifier votre code existant.

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

Dans ces deux articles, il vous montre comment configurer ce modèle et explique également pourquoi il est utile. En utilisant ce modèle, vous obtenez la mise en cache sans que votre code existant ne voit la logique de mise en cache. Essentiellement, vous utilisez le référentiel mis en cache comme s'il s'agissait d'un autre référentiel.

Brendan Enrick
la source
1
Grands messages! Merci d'avoir partagé!!
Mark Good du
Liens morts au 31/08/2013.
CBono
Pouvez-vous s'il vous plaît partager un lien où je peux lire sur la mise en cache des applications basée sur les valeurs clés. Je ne trouve pas de liens.
Incassable
4

AppFabric Caching est distribué et une technique de mise en cache en mémoire qui stocke les données dans des paires clé-valeur en utilisant la mémoire physique sur plusieurs serveurs. AppFabric fournit des améliorations de performances et d'évolutivité pour les applications .NET Framework. Concepts et architecture

Arun Duth
la source
Ceci est spécifique à Azure, pas à ASP.NET MVC en général.
Henry C
3

Prolonger la réponse de @Hrvoje Hudo ...

Code:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

Exemples

Mise en cache d'un seul élément (lorsque chaque élément est mis en cache en fonction de son ID, car la mise en cache de l'ensemble du catalogue pour le type d'élément serait trop intensive).

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

Mettre tout en cache

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

Pourquoi TId

Le deuxième assistant est particulièrement agréable car la plupart des clés de données ne sont pas composites. Des méthodes supplémentaires pourraient être ajoutées si vous utilisez souvent des clés composites. De cette façon, vous évitez de faire toutes sortes de concaténation de chaînes ou de string.Formats pour obtenir la clé à passer à l'assistant de cache. Cela facilite également le passage de la méthode d'accès aux données car vous n'avez pas à passer l'ID dans la méthode wrapper ... le tout devient très concis et cohérent pour la majorité des cas d'utilisation.

smdrager
la source
1
Vos définitions d'interface ne contiennent pas le paramètre "durationInMinutes". ;-)
Tech0
3

Voici une amélioration de la réponse de Hrvoje Hudo. Cette implémentation présente quelques améliorations clés:

  • Les clés de cache sont créées automatiquement en fonction de la fonction de mise à jour des données et de l'objet transmis qui spécifie les dépendances
  • Passer le laps de temps pour toute durée de cache
  • Utilise un verrou pour la sécurité du fil

Notez que cela a une dépendance sur Newtonsoft.Json pour sérialiser l'objet dependOn, mais qui peut être facilement remplacé par toute autre méthode de sérialisation.

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

Usage:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);
DShook
la source
2
Le if (item == null)devrait être à l'intérieur de la serrure. Maintenant, lorsque ifc'est avant le verrouillage, des conditions de concurrence peuvent se produire. Ou encore mieux, vous devez conserver ifavant le verrou, mais vérifiez à nouveau si le cache est toujours vide comme première ligne à l'intérieur du verrou. Parce que si deux threads arrivent en même temps, ils mettent tous les deux à jour le cache. Votre verrou actuel n'est pas utile.
Al Kepp
3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}
Chau
la source
3
Pensez à ajouter des explications
Mike Debela
2

Je l'ai utilisé de cette façon et cela fonctionne pour moi. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx informations sur les paramètres pour system.web.caching.cache.add.

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}
user3776645
la source
des votes supplémentaires pour des choses entièrement qualifiées avec son espace de noms complet !!
Ninjanoel
1

J'utilise deux classes. Tout d'abord, l'objet principal du cache:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

La seconde est la liste des objets de cache:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}
Berezh
la source
0

Je dirai que la mise en œuvre de Singleton sur ce problème de données persistant peut être une solution à ce problème au cas où vous trouveriez des solutions précédentes beaucoup plus compliquées.

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton
GeraGamo
la source
Cela a parfaitement fonctionné pour moi, c'est pourquoi je recommande cela à tous ceux qui peuvent être aidés par cela
GeraGamo
0
HttpContext.Current.Cache.Insert("subjectlist", subjectlist);
Md. Akhtar Uzzaman
la source
-8

Vous pouvez également essayer d'utiliser la mise en cache intégrée à ASP MVC:

Ajoutez l'attribut suivant à la méthode de contrôleur que vous souhaitez mettre en cache:

[OutputCache(Duration=10)]

Dans ce cas, l'ActionResult sera mis en cache pendant 10 secondes.

Plus à ce sujet ici

qui
la source
4
OutputCache est pour le rendu d'Action, la question concernait la mise en cache des données et non la page.
Coolcoder
il est hors sujet mais OutputCache met également en cache les données de la base de données
Muflix