Entity Framework 4 Series : Caching

6. December 2009

Dans mon article précédent nous avons vu comment mettre en place le Pattern Repository. Aujourd’hui nous allons voir comment améliorer les performances et implémenter une surcouche de mise en Cache au dessus de cette Repository Generique.

image

Pour cela nous allons commencer par modifier l’interface IRepository<T> en rajoutant quelques méthodes qui nous permettrons d’obtenir les informations d’état de l’entité.

public interface IRepository<T> : IDisposable where T : class
{
IQueryable<T> Fetch();
IEnumerable<T> GetAll();
IEnumerable<T> Find(Func<T, bool> predicate);
T Single(Func<T, bool> predicate);
T First(Func<T, bool> predicate);
void Add(T entity);
void Delete(T entity);
void Attach(T entity);
void SaveChanges();
void SaveChanges(SaveOptions options);

/// <summary>
/// Get Object State Entries
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
IEnumerable<ObjectStateEntry> GetObjectStateEntries(EntityState state);

/// <summary>
/// Get Object State Entry
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
ObjectStateEntry GetObjectStateEntry(T entity);
}

Nous allons ensuite implémenter ces méthodes dans notre Repository Generique en utilisant l’ObjectStateManager.

public IEnumerable<ObjectStateEntry> GetObjectStateEntries(EntityState state)
{
return _context.ObjectStateManager.GetObjectStateEntries(state);
}

public ObjectStateEntry GetObjectStateEntry(T entity)
{
return _context.ObjectStateManager.GetObjectStateEntry(entity);
}

Pour l’implémentation du Cache nous allons utiliser une interface ICache qui nous permettra ensuite d’implémenter nos providers de cache.

public interface ICache
{
int Count { get; }

void Clear();

bool Contains(string key);

T Get<T>(string key);

void Set<T>(string key, T value);

void Remove(string key);

IEnumerable<T> GetAll<T>();
}

Pour les besoins de l’article j’ai crée un provider de cache SimpleCache qui stock juste les objets dans un dictionnaire. Par la suite vous pourrez implémenter votre propre provider de cache pour utiliser System.Web.Caching (ASP.NET), Enterprise Library Caching Block ou autres.

public class SimpleCache : ICache
{
private static IDictionary<string, object> CachedObjects { get; set; }

public SimpleCache()
{
CachedObjects = new Dictionary<string, object>();
}

#region ICache Members

public void Clear()
{
CachedObjects.Clear();
}

public bool Contains(string key)
{
return CachedObjects.ContainsKey(key);
}

public T Get<T>(string key)
{
object o;

CachedObjects.TryGetValue(key, out o);

return (T)o;
}

public void Set<T>(string key, T value)
{
if (Contains(key))
Remove(key);
CachedObjects.Add(key, value);
}

public void Remove(string key)
{
if (!Contains(key))
return;
CachedObjects.Remove(key);
}

public IEnumerable<T> GetAll<T>()
{
return CachedObjects.Select(p => p.Value).Cast<T>().AsEnumerable<T>();
}

public int Count
{
get { return CachedObjects.Count; }
}

#endregion
}

 

 

 

Maintenant passons aux choses sérieuses :) L’implémentation de notre surcouche a Repository<T> utilisant le Cache.

Pour ce faire nous allons créer une CachingRepository<T> qui implémente IRepository<T>, cette classe prendra en paramètre de constructeur une instance de Repository<T> afin de bénéficier de sa logique de traitement interne ainsi qu’un ICache qui sera notre instance de provider de Cache.

public class CachingRepository<T> : IRepository<T> where T : class
{
private IRepository<T> _innerRepository;
private static ICache _cache;

public CachingRepository(IRepository<T> innerRepository)
: this(innerRepository, new SimpleCache())
{
}

public CachingRepository(IRepository<T> innerRepository, ICache cache)
{
_innerRepository = innerRepository;
_cache = cache;
}
}

L’idée maintenant est de maintenir le cache a jours lors des appels aux méthodes GetAll(), First, Single mais également lors des Add(), Delete() et Updates.

Toute la logique de mise en cache va être fais lors de l’appel a SaveChanges, puisque c’est cette méthode qui va persister les données sur la base de données. C’est donc ici que nous allons faire appel a nos méthodes GetObjectStateEntries que nous avons déclaré un peu plus haut dans l’interface IRepository afin de récuperer l’état de nos entités (Added, Modified, Deleted, Unchanged) via l’énumeration EntityState.

 

public void SaveChanges()
{
SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
}

public void SaveChanges(SaveOptions options)
{
// Get Entities State
var addedEntries = GetObjectStateEntries(EntityState.Added);
var modifiedEntries = GetObjectStateEntries(EntityState.Modified);
var deletedEntries = GetObjectStateEntries(EntityState.Deleted);

// We have to get cache item before SaveChanges applies to database for Deleted Entities since
// there status will not be available after this.
var cacheItemsToDelete = GetCacheItems(deletedEntries);

// Persist to Database
_innerRepository.SaveChanges();

// Perform Cache Updates
var cacheItemsToAdd = GetCacheItems(addedEntries);
var cacheItemsToUpdate = GetCacheItems(modifiedEntries);

CacheItems(cacheItemsToAdd, EntityState.Added);
CacheItems(cacheItemsToUpdate, EntityState.Modified);
CacheItems(cacheItemsToDelete, EntityState.Deleted);
}

#region Caching Methods

/// <summary>
/// Get Unique Cache Key based on EntityKey
/// </summary>
/// <param name="entityKey"></param>
/// <returns></returns>
private string GetCacheKey(EntityKey entityKey)
{
if (entityKey == null)
throw new ArgumentNullException("Entity cannot be null");

if (entityKey.EntityKeyValues == null)
return string.Empty;

var cacheKey = new StringBuilder();

cacheKey.AppendFormat("{0}:", typeof(T).Name);

foreach (var keyValue in entityKey.EntityKeyValues)
{
cacheKey.AppendFormat("{0}", keyValue);
}

return cacheKey.ToString();
}

/// <summary>
/// Cache Entities
/// </summary>
/// <param name="entities"></param>
private void CacheEntities(IEnumerable<T> entities)
{
var entries = new List<ObjectStateEntry>();

foreach (var entity in entities)
{
var entry = GetObjectStateEntry(entity);
entries.Add(entry);
}

var cacheItems = GetCacheItems(entries);

CacheItems(cacheItems, EntityState.Unchanged);
}

/// <summary>
/// Perform Cache Update
/// </summary>
/// <param name="key"></param>
/// <param name="entity"></param>
/// <param name="state"></param>
private void CacheItem(string key, T entity, EntityState state)
{
switch (state)
{
case EntityState.Added:
_cache.Set(key, entity);
break;
case EntityState.Modified:
_cache.Set(key, entity);
break;
case EntityState.Deleted:
_cache.Remove(key);
break;
default:
_cache.Set(key, entity);
break;

}
}

/// <summary>
/// Perform Cache Updates
/// </summary>
/// <param name="items"></param>
/// <param name="state"></param>
private void CacheItems(IDictionary<string, T> items, EntityState state)
{
foreach (var item in items)
{
CacheItem(item.Key, item.Value, state);
}
}

/// <summary>
/// Get Cache Items
/// </summary>
/// <param name="entries"></param>
/// <param name="state"></param>
/// <returns></returns>
private IDictionary<string, T> GetCacheItems(IEnumerable<ObjectStateEntry> entries)
{
var items = new Dictionary<string, T>();
foreach (var entry in entries)
{
var cacheKey = GetCacheKey(entry.EntityKey);
items.Add(cacheKey, (T)entry.Entity);
}
return items;
}

#endregion

 

Et voila maintenant nous pouvons continuer a utiliser notre repository normalement sans avoir a ce soucier de la façon dont les données sont mises en cache.

Ce n’est qu’un début d’implémentation de cache avec Entity Framework 4, qui peut très certainement être amélioré mais c’est un début.

 

Code source de l’article :




Shout it

C#, Visual Studio 2010, Entity Framework , ,

Comments

littlesteps
littlesteps
12/16/2009 9:11:43 PM #
Bonjour,

Bravo pour votre article, il est très intéressant et bien documenté.

J'utilise depuis peu EF (version .NET 3.5 SP1), et je souhaite justement optimiser les performances en mettant en place des mécanismes de cache.

Ce que j'observe, c'est que les contraintes de performances sont essentiellement liées aux requêtes vers la source de données (SQL Server 2008 en l'occurence).

Dans un contexte d'application client-serveur(sous-entendu avec accès concurrents à la base de données par N clients), la gestion de cache au sein de l'ObjectContext est insuffisante, puisque le cache peut ne plus être à jour suite à la modification d'une donnée dans la base par un autre client.
Dans ce scénario (qui est le plus fréquent je suppose puisqu'on développe plus rarement une application avec une base de données non partagée) Entity Framework propose-t-il des mécanismes pour gérer un tel cache ?

J'avais commencé à implémenter un mécanisme de cache basique, mais le souci est que le cout d'interroger la base pour savoir si le cache est à jour (simple comparaison de timestamp) est bien souvent équivalent à récupérer la donnée elle-même, sauf dans le cas du chargement d'objets enfants à l'objet considéré (je fais du lazzy loading).

Avez-vous des pistes sur ce sujet ?


Bien cordialement,

Littlesteps
yb
12/16/2009 9:48:19 PM #
Bonjour,

Merci de votre commentaire.

Si je comprend bien vous avez besoin d'un système de notification du cache lorsque la base de donnée a été modifiée par un tier.

Vous devriez regarder le SqlDependency, qui permet de faire du change tracking au niveau de SQL Server et ainsi d'avoir des énevenements lors de la modification de la base de donnée, ce qui vous permettra ensuite de mettre a jours votre cache.

msdn.microsoft.com/.../...lient.sqldependency.aspx
littlesteps
littlesteps
12/16/2009 11:39:13 PM #
Re-Bonjour,

Merci pour votre réponse.

Effectivement, j'ai besoin de ce type de cache, car le temps de réponse de l'application cliente va dépendre clairement du nombre d'allers-retours vers le tiers "base de données" (coût du transport sur le réseau + coût de la requête SQL).
Après quelques recherches, j'ai pu voir qu'a priori EF ne propose pas de support de SqlDependency (SqlCacheDependency en ASP .NET) ni encore du Change Tracking par Sync Framework.

Ce post donne quelques pistes pour une intégration de SqlCacheDependency avec EF, mais rien de très convaincant a priori : social.msdn.microsoft.com/.../44a980f4-c8db-4dce-9487-4c519efe5419

Je vais regarder plus précisément si ce sujet est dans le scope d'EF 4, mais s'il n'est pas du tout adressé dans la beta actuelle, il y a peu de chance qu'il soit abordé dans la release finale...
4/7/2010 8:13:16 AM #
Constructeur Lannion

Article intéressant, merci pour ces infos. Bonne continuation. Publication sur notre site.