2016-02-24 1 views
0

J'ai besoin d'un mécanisme de cache générique pour les appels de méthodes.Comment générer une clé qui peut être sortie d'un arbre d'expression d'appel de méthode en C#?

Dire que j'ai un appel uncached, comme ceci:

var output = impl.GetById(guid); 

je suis venu avec un utilitaire de cache qui me permet d'écrire:

var output = _cache.Cache((i) => i.GetById(guid)); 

_cache = new Cache(impl). L'idée est que cache.Cache retournera une valeur mise en cache si GetById(guid) ne change pas. Pour fonctionner correctement, j'ai besoin de produire une clé fiable sur i.GetById(guid). Comment je fais ça?

Voici ma mise en œuvre naïve:

public class Cache<I> 
    { 
     private I _impl; 
     private MemoryCache _cache; 

     public Cache(I impl, MemoryCache cache = null) 
     { 
      _impl = impl; 
      _cache = cache ?? MemoryCache.Default; 
     } 

     public R Cached<R>(Expression<Func<I, R>> expr) 
     { 
      var keyBuilder = new StringBuilder(); 
      var methodExpr = expr.Body as MethodCallExpression; 
      keyBuilder.Append(methodExpr.Method.Name); 
      var args = new object[methodExpr.Arguments.Count]; 
      for (int i = 0; i < args.Length; ++i) 
      { 
       var lambdaExp = Expression.Lambda(methodExpr.Arguments[i]); 
       args[i] = lambdaExp.Compile().DynamicInvoke(); 
       keyBuilder.AppendFormat(" {0}:{1}", 
        (args[i] ?? "").GetType().Name, 
        RuntimeHelpers.GetHashCode(args[i])); 
      } 

      var key = keyBuilder.ToString(); 
      var lazy = new Lazy<object>(() => 
      { 
       try { return methodExpr.Method.Invoke(_impl, args); } 
       catch (TargetInvocationException e) { return e.InnerException; } 
       catch (Exception e) { return e; } 
      }); 
      var offset = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(5)); 
      var oldLazy = (Lazy<object>)_cache.AddOrGetExisting(key, lazy, offset); 
      object value = (oldLazy != null) ? oldLazy.Value : lazy.Value; 

      var exception = value as Exception; 
      if (exception != null) throw exception; 

      return (R)value; 
     } 
    } 

Mise à jour:

Juste pour rendre les choses plus claires. Mon intention est d'utiliser ce mécanisme de mise en cache pour mettre en cache les appels DB. Dans mon cas, les arguments sont principalement des types primitifs (+ string) et il y a toujours une seule sortie (l'objet retourné).

Voici un exemple d'interface:

public interface ITransactionDb { 
    Trasaction GetById(Guid id); 
    IList<Transaction> ListTransactions(Datetime start, Datetime end, string origin = null); 
} 

Cependant, je voudrais cette conception pour être suffisamment robuste pour le cache, par exemple, les appels RPC dans lequel le paramètre d'entrée de cas est un non primitif.

Al alternatif et la conception beaucoup plus simple pourrait être:

public class Cache { 
     public R Cached<R>(Func<R> method) 
     { 
      var key = GenerateKey(method.Method); 
      return GetOrAdd(key, method); 
     } 

     public R Cached<T1, R>(Func<T1, R> method, T1 t1) 
     { 
      var key = GenerateKey(method.Method, t1); 
      return GetOrAdd(key,() => method(t1)); 
     } 

     public R Cached<T1, T2, R>(Func<T1, T2, R> method, T1 t1, T2 t2) 
     { 
      var key = GenerateKey(method.Method, t1, t2); 
      return GetOrAdd(key,() => method(t1, t2)); 
     } 
    } 

appel Cached ressemblerait à ceci:

var transaction = cache.Cache(impl.GetById, guid); 
+0

Pouvez-vous publier votre classe 'I'? – Sakura

+3

Remarque: en général, vous ne pouvez pas utiliser HashCode pour la clé de cache car elle ne garantit pas l'unicité ... Vous devez utiliser une sorte de convention/interface/commutateur pour obtenir des composants de clé de cache fiables de chaque type. –

+1

J'ai du mal à comprendre ce que vous obtenez ici. Êtes-vous en train d'essayer de mémoriser l'appel? –

Répondre

0

peut vos paramètres d'entrée être les deux types de valeurs et types de référence? Avez-vous une stratégie de comparaison d'égalité définie pour les types de référence? Pouvez-vous produire une valeur de chaîne unique (à partir du point de vue de l'égalité) à partir de n'importe quelle instance de type de référence? Si oui, vous pouvez créer une clé sous forme de chaîne en utilisant toute forme acceptable de séparation de valeurs.

Si vous ne pouvez pas choisir un caractère de séparation tel que '_' ou '|' alors vous pouvez obtenir plus de créativité et de définir un format, tel que

« 3_4_abcd_6_12_456_10_DHXSS94HGF »

qui déchiffre: 3 - nombre de paramètres 4 - longueur du premier paramètre ABCD - valeur du premier paramètre 6 - longueur du second paramètre 12_456 - valeur du second paramètre 10 - longueur du troisième paramètre DHXSS94HGF - valeur du troisième paramètre

que la variation de ce paramètre et représente v aleurs comme chaîne de requête a = b = abcd & 12_456 & c = DHXSS94HGF

mais encore une fois il va à votre restriction sur le jeu de caractères.Vous pouvez obtenir un peu de créativité sur d'autres façons multiples de générer une chaîne représentant un ensemble de paramètres, mais le plus difficile ici est de pouvoir définir la conversion de l'objet en une chaîne qui génère une valeur de chaîne identique pour deux instances différentes avec logiquement la même valeur. [Update] en utilisant des séparateurs est assez important pour éviter les collisions indésirables. alors imaginez que vous avez

string Func(int a, int b) { ... } 
// then 
Func(12,3); 
Func(1,23); 

si vous les deux générerait pas utiliser des séparateurs de valeur clé « 123 » pendant que vous voulez probablement deux clés différentes « de 12_3 » et « 1_23 »

+0

Ceci cible les appels à DB, donc les arguments sont principalement des primitives (+ chaîne). Il y a une seule sortie (retour). – Gatis

+0

puis ajoutez-les ensemble. Si vous pensez qu'il va générer trop long d'une clé, obtenez un hachage cryptographique dessus, comme sha256 ou sha512 qui donnerait un assez bon caractère unique. Cependant, il semble que la classe MemoryCache n'applique aucune limite de longueur sur la clé, donc elle ne devrait pas être nécessaire. Choisissez un caractère de séparation qui est peu susceptible d'être présent dans la valeur et de les concaténer. – aiodintsov

0

Déplacer le problème sérialisation argument . Si votre méthode est stable, et que vous ne connaissez rien aux paramètres, vous avez besoin d'une sérialisation. Vous ne pourrez évidemment pas l'utiliser avec, par exemple, Streams, mais cela pourrait convenir à votre cas d'utilisation ...