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));
Où _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);
Pouvez-vous publier votre classe 'I'? – Sakura
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. –
J'ai du mal à comprendre ce que vous obtenez ici. Êtes-vous en train d'essayer de mémoriser l'appel? –