如何在 C# 中从方法调用表达式树中生成可哈希键

本文关键字:哈希键 表达式 方法 调用 | 更新日期: 2023-09-27 18:36:00

我需要一种通用的缓存机制来进行方法调用。

假设我有一个未缓存的调用,如下所示:

var output = impl.GetById(guid);

我想出了一个缓存实用程序,允许我编写:

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

哪里_cache = new Cache(impl).这个想法是,如果GetById(guid)没有更改,cache.Cache将返回缓存的值。

为了使它正常工作,我需要从i.GetById(guid)中生成一个可靠的密钥。我该怎么做?

这是我幼稚的实现:

    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;
        }
    }

更新:

只是为了让事情更清楚。我的目的是使用此缓存机制来缓存数据库调用。在我的例子中,参数大多是原始类型(+字符串),并且总是有一个输出(返回的对象)。

下面是一个接口示例:

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

但是,我希望这种设计足够强大,可以缓存 RPC 调用,在这种情况下,输入参数是非原语。

其他和更简单的设计可能是:

    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));
        }
   }

缓存的调用如下所示:

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

如何在 C# 中从方法调用表达式树中生成可哈希键

将问题移至参数序列化。如果您的方法很稳定,并且您对参数一无所知,则需要可序列化性。您显然无法将其与例如 Streams 一起使用,但对于您的用例来说可能没问题......

输入参数可以同时是值类型和引用类型吗?您是否为引用类型定义了相等比较策略?是否可以从任何引用类型实例生成唯一的字符串值(从相等的角度来看)?如果是,则可以使用任何可接受的值分隔形式将键构建为字符串。

如果您不能选择分隔字符(例如"_"或"|"),则可以更具创意并定义格式,例如

"3_4_abcd_6_12_456_10_DHXSS94HGF"

其中破译:3 - 参数数量4 - 第一个参数的长度abcd - 第一个参数的值6 - 第二个参数的长度12_456 - 第二个参数的值10 - 第三个参数的长度DHXSS94HGF - 第三个参数的值

作为变体,将参数和值表示为查询字符串a=abcd&b=12_456&c=DHXSS94HGF

但它再次涉及您对字符集的限制。

您可以在其他多种可能的方式来生成表示参数集的字符串方面有所创意,但这里最困难的部分是能够定义从对象到字符串的转换,该字符串生成一个字符串值,该字符串值对于两个具有逻辑相同值的不同实例。

[更新] 使用分隔符对于避免不必要的冲突有些重要。 所以想象一下你有

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

如果您不使用值分隔符,则两者都会生成键"123",而您可能想要两个不同的键"12_3"和"1_23"