Tipps und Tricks rund um .NET und Visual Studio

Einfach statt kompliziert: Cachen per CacheManager-Implementierung
Keine Kommentare

Dr. Holger Schwichtenberg (MVP) teilt in der Kolumne „.NETversum“ sein Expertenwissen rund um .NET-Tools und WPF mit. In dieser Ausgabe geht es um den .NET-CacheManager.

Bei Codereviews und der Analyse von Performanceproblemen in Anwendungen fällt dem Autor dieses Tipps immer wieder eins auf: Es werden viel zu oft die immer gleichen Daten von der Datenbank abgeholt, statt die Daten einfach mal im RAM zu halten. Caching ist oft die einfachste Lösung für viele Probleme!

Nun gibt es seit .NET 4.0 die Caching-Mechanismen in System.Runtime.Caching. Aber anscheinend sind diese noch nicht einfach genug, damit alle sie nutzen. Ja, man muss bei begrenztem Caching (z.B. mit Zeitbegrenzung) vor jedem Zugriff prüfen, ob die Daten noch im Cache sind, und sie ggf. dann wieder laden. Und jeder Zugriff auf den Cache erfolgt über einen Indexer mit Zeichenkette mit der damit verbundenen Gefahr, sich zu vertippen. Natürlich legt man dafür dann besser eine Konstante an, aber dann kommen noch mehr Codezeilen zusammen. Und tun denn alle Entwickler das dann auch immer konsequent? Hier setzt die CacheManager-Implementierung aus diesem Beispiel an (Listing 1). Man will in seiner Geschäftslogik einfach schreiben können, und dabei erwarten, dass nur einmal auf die Datenbank zugegriffen wird:

var softwarearchitektenMitDr1 = dataAccess.
  GetAll("Dr.");
// ...
var softwarearchitektenMitDr2 = dataAccess.
  GetAll("Dr.");
// ...
var softwarearchitektenMitDr3 = dataAccess.
  GetAll("Dr.");

In der Datenzugriffsschicht möchte ich es einfach haben. GetSoftwarearchitektSet() nutzt die Klasse CacheManager unter Angabe des Rückgabetyps und der Speicherdauer in Sekunden. Dann wird im CacheManager eine Methode Get() aufgerufen. Hier wird der Name für das Caching einmalig angegeben. Der zweite Parameter ist eine Callback-Methode, die die Daten lädt. Weitere Parameter sind optionale Parameter für diese Lademethode, die an die Lademethode übergeben werden:

public List<Softwarearchitekt> GetSoftwarearchitektSet(string Suchbbegriff)
{
  var cm = new CacheManager<List<Softwarearchitekt>>(60);
  return cm.Get("Softwarearchitekten", GetSoftwarearchitektSetFromDatabase, Suchbbegriff);
}

Alternativ kann man auf den Namen für den Cacheeintrag auch ganz verzichten. Der CacheManager nimmt dann den Namen des generischen Typs als Cacheeintragsname:

return cm.Get(GetSoftwarearchitektSetFromDatabase, Suchbbegriff);

Wie man hier eigentlich zu den Daten kommt, ist dann die Sache der Lademethode. Im nachfolgend dargestellten Fall nutzt sie einen Entity-Framework-Kontext. Diese Methode ist in der Regel privat. Die Geschäftslogik sollte sie nicht direkt aufrufen können.

private List<Softwarearchitekt> GetSoftwarearchitektSetFromDatabase(string[] wert)
{
var suchbegriff = wert[0];
  var q = (from x in EFKontext.Entwickler where x.Typ == Entwicklertypen.Softwarearchitekt && x.Name.StartsWith(suchbegriff) select x);
  return q.ToList();
}

Es ist Aufgabe der CacheManager-Implementierung, zu entscheiden, ob die Lademethode gerufen werden muss oder nicht. Zudem bietet der CacheManager Folgendes:

  • Eine Standardzwischenspeicherzeit, die abhängig von der Umgebung (Debugging oder normale Ausführung) ist
  • Die Möglichkeit, alternativ zum Default-Memory-Cache (MemoryCache.Default) eine benutzerdefinierte MemoryCache-Instanz zu nutzen mit einstellbaren Werten für cacheMemoryLimitMegabytes, physical-MemoryLimitPercentage und pollingInterval
  • Eine statische Methode Clear(), die den Default-Memory-Cache und alle benutzerdefinierten Caches löscht
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Caching;
 
namespace ITVisions.Caching
{
 
  public class CacheManager
  {
 
    public static List<MemoryCache> AllCaches = new List<MemoryCache>();
 
    public static void Clear()
    {
      MemoryCache.Default.Dispose();
      foreach (var c in AllCaches)
      {
        c.Dispose();
      }
    }
  }
 
 
  /// <summary>
  /// neu ab v6.8 (29.8.2014)
  /// </summary>
  public class CacheManager<T> where T : class
 {
 
    /// <summary>
    /// Normale Cache-Zeit
    /// </summary>
    public static int DefaultCacheZeit = 60 * 10; // 10 Minuten
 
    /// <summary>
    /// Veringerte Cache-Zeit im Debug-Modus
    /// </summary>
    public static int DefaultCacheZeit_DEBUG = 10; // 10 Sekunden
 
    private int Seconds = DefaultCacheZeit;
 
 
    public MemoryCache cache { get; set; } = MemoryCache.Default;
 
    /// <summary>
    /// Erzeugte CacheManager mit eigener MemoryCache.Default
    /// </summary>
    public CacheManager()
    {
      if (System.Diagnostics.Debugger.IsAttached
)
      {
        this.Seconds = DefaultCacheZeit_DEBUG;
      }
      else
      {
        this.Seconds = DefaultCacheZeit;
      }
    }
 
    /// <summary>
    /// Erzeugte CacheManager mit eigener MemoryCache.Default
    /// </summary>
    public CacheManager(int seconds)
    {
      this.Seconds = seconds;
    }
 
 
    /// <summary>
    /// Erzeugte CacheManager mit eigener MemoryCache-Instanz
    /// </summary>
    /// <param name="seconds">Gets or sets the maximum memory size, in megabytes, that an instance of a MemoryCache object can grow to.</param>
    /// <param name="cacheMemoryLimitMegabytes"></param>
    /// <param name="physicalMemoryLimitPercentage">Gets or sets the percentage of memory that can be used by the cache.</param>
    /// <param name="pollingInterval">Gets or sets a value that indicates the time interval after which the cache implementation compares the current memory load against the absolute and percentage-based memory limits that are set for the cache instance.</param>
    public CacheManager(int seconds, int cacheMemoryLimitMegabytes, int physicalMemoryLimitPercentage, TimeSpan pollingInterval)
    {
      var config = new System.Collections.Specialized.NameValueCollection();
      //config.Add("CacheMemoryLimitMegabytes",cacheMemoryLimitMegabytes.ToString());
      //config.Add("PhysicalMemoryLimitPercentage",physicalMemoryLimitPercentage.ToString());
      //config.Add("PollingInterval",pollingInterval.ToString());
      cache = new MemoryCache("CustomMemoryCache_" + Guid.NewGuid().ToString(), config);
      CacheManager.AllCaches.Add(cache);
      this.Seconds = seconds;
    }
 
 
    public T Get(string name)
    {
      object objAlt = cache[name];
      return objAlt as T;
    }
 
 
    /// <summary>
    /// Holt Element aus Cache oder Datenquelle. Name wird der Name des Generischen Typs
    /// </summary>
    /// <returns>Daten vom Typ T</returns>
    public T Get(Func<string[], T> loadDataCallback, params string[] args)
    {
      return Get(typeof(T).FullName, loadDataCallback, args);
    }
 
    /// <summary>
    /// Holt Element aus Cache oder der Datenquelle mithilfe der Lademethode.
    /// </summary>
    /// <param name="loadDataCallback">Lademethode mit Signatur T Name(string[] wert)</param>
    /// <param name="obj">Das zu speichernde Objekt</param>
    /// <returns>Daten vom Typ T</returns>
    public T Get(string name, Func<string[], T> loadDataCallback, params string[] args)
    {
      string aktion = "";
      object obj = cache[name];
      //Console.WriteLine(cache.ToNameValueString());
      if (obj == null)
      {
        aktion = "Load data";
        obj = loadDataCallback(args);
        Save(name, obj as T);
      }
      else
      {
        aktion = "Cache hit";
      }
 
      Trace.WriteLine(aktion + " for " + name + " (" + cache.GetCount() + " Cache Elements/Cache-Dauer: " + Seconds + "sec");
 
      return obj as T;
    }
 
 
    /// <summary>
    /// Speichert ein Objekt im Cache
    /// </summary>
    /// <param name="name">Name für das Objekt</param>
    /// <param name="obj">Das zu speichernde Objekt</param>
    public void Save(string name, T obj)
    {
      if (obj == null) return;
      object objAlt = cache[name];
      if (objAlt == null)
      {
        CacheItemPolicy policy = new CacheItemPolicy();
        policy.AbsoluteExpiration = DateTime.Now.AddSeconds(Seconds);
        cache.Set(name, obj, policy);
      }
    }
  }
}

Buchempfehlungdotnet_praxis

In den vergangenen Jahren haben Dr. Holger Schwichtenberg und Manfred Steyer im „Windows Developer“ regelmäßig Tipps und Tricks aus ihrem Alltag als Softwareentwickler, Berater und Trainer für .NET- und webbasierte Anwendungen veröffentlicht. Das vorliegende Buch ist eine aktualisierte und deutlich erweiterte Zusammenfassung dieser Tipps und Tricks. Es umfasst sowohl das .NET Framework und seine zahlreichen Teilbibliotheken als auch die beliebte JavaScript-Bibliothek AngularJS sowie die Entwicklungsumgebung Visual Studio und ergänzende Werkzeuge.

Mehr Infos: www.entwickler-press.de/Dotnet-Praxis

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -