【问题标题】:Caching Compiled Expression Delegates缓存已编译的表达式委托
【发布时间】:2016-03-09 18:05:05
【问题描述】:

背景故事:在整个项目中,我们不断发现自己使用System.ComponentModel.DataAnnotations.Validator 对象以及属性来验证传递给我们 API 的属性。 Validator 对象需要传入属性名称和值,这是公平的,但我们都被传递的[不断增加] 数量的 魔术字符串 搞砸了对于这些属性名称。这不仅会带来错误输入属性名称的风险,还会在其中一个属性被重命名的情况下降低可维护性(同样,它们太多了)。

为了自动解析成员名称,我创建了这个辅助方法(为了简单起见,假设我只处理引用类型的属性,因此有P: class 约束):

public static P ValidateProperty<T, P>(T obj, Expression<Func<T, P>> member, List<ValidationResult> results, Func<P, P> onValidCallback = null)
    where T : class
    where P : class
{
    var expr = member.Compile();
    var memberName = ((MemberExpression)member.Body).Member.Name;
    var value = expr(obj);

    bool isValid = Validator.TryValidateProperty(value, new ValidationContext(obj) { MemberName = memberName }, results);
    return isValid ? (onValidCallback != null ? onValidCallback(value) : value) : null;
}

我只是这样称呼它:

var value = ValidateProperty(myObject, x => x.PropertyFoo, errors, result => result.Trim());

这就像一个魅力,不涉及传递任何魔术字符串。

示例请求如下所示:

public class Request
{
    public class C1
    {
        Property1;
        Property2;
        ...
    }
    public class C2
    {
        Property1;
        Property2;
        ...
    }
    public class C3
    {
        Property1;
        Property2;
        ...
    }
    ...
}

然而,我在这里担心的是member.Compile() 的性能影响。由于T, P 有许多不同的可能排列(C1, Property1C1, Property2C2, Property1 等)我将无法缓存已编译的表达式并在下一次调用时执行它,除非TP 属于同一类型,这种情况很少发生。

我可以通过将Expression&lt;Func&lt;T, P&gt;&gt; member 更改为Expression&lt;Func&lt;T, object&gt;&gt; member 来优化这一点,这样现在我只需将每个T 类型的表达式缓存一次(即C1C2 等)

我想知道是否有人对更好的方法有任何意见,还是我试图“过度设计”问题?对于一遍又一遍地传递魔术字符串的情况,是否有一种常见的模式?

【问题讨论】:

  • 这会让你回到神奇的字符串,但你是否考虑过新的 C#6 nameof 运算符而不是 od 表达式?
  • 伙计,nameof 正是我所需要的,但不幸的是,我们现在被困在 C#5 上......

标签: c# lambda expression-trees


【解决方案1】:

另一种选择:与其使用 C# 6 功能或编译,不如使用反射。现在您正在用编译性能下降换取反射性能下降(可能更少)。

...
var property = (PropertyInfo) ((MemberExpression)member.Body).Member;
var propertyName = property.Name;
var value =  property.GetValue(obj, new object[0]);
...

【讨论】:

    【解决方案2】:

    嗯,你是对的,Expression.Compile 确实有很大的性能开销。如果性能确实是一个问题,并且另一个答案中提到的反射方法仍然与您有关,那么您可以通过以下方式实现编译委托缓存:

    public static class ValidateUtils
    {
        static readonly Dictionary<Type, Dictionary<string, Delegate>> typeMemberFuncCache = new Dictionary<Type, Dictionary<string, Delegate>>();
    
        public static P ValidateProperty<T, P>(this T obj, Expression<Func<T, P>> member, List<ValidationResult> results, Func<P, P> onValidCallback = null)
            where T : class
            where P : class
        {
            var memberInfo = ((MemberExpression)member.Body).Member;
            var memberName = memberInfo.Name;
            Func<T, P> memberFunc;
            lock (typeMemberFuncCache)
            {
                var type = typeof(T);
                Dictionary<string, Delegate> memberFuncCache;
                if (!typeMemberFuncCache.TryGetValue(type, out memberFuncCache))
                    typeMemberFuncCache.Add(type, memberFuncCache = new Dictionary<string, Delegate>());
                Delegate entry;
                if (memberFuncCache.TryGetValue(memberName, out entry))
                    memberFunc = (Func<T, P>)entry;
                else
                    memberFuncCache.Add(memberName, memberFunc = member.Compile());
            }
            var value = memberFunc(obj);
            bool isValid = Validator.TryValidateProperty(value, new ValidationContext(obj) { MemberName = memberName }, results);
            return isValid ? (onValidCallback != null ? onValidCallback(value) : value) : null;
        }
    }
    

    顺便说一句,我会删除P : class 约束,因此也允许验证数字、日期等值,但无论如何。

    【讨论】:

    • 虽然不想传递字符串,但这似乎有点矫枉过正......?
    • 好吧,正如我所说,nameof 是最好的。除此之外,我不会称其为矫枉过正,这与dynamic 在幕后所做的非常相似。当然,在您遇到性能问题时才使用它。反射方法可能是一种权衡,我打算将它作为一种选择向你推荐,但看到另一个答案是针对它的。
    【解决方案3】:

    在 C# 6 (VS2015) 中,您可以使用nameof。对 ValidateProperty 方法的调用涉及一个额外的参数(属性名称与属性值),它引入了一些冗余,但解决了动态编译一堆东西的潜在性能问题。

    var value = ValidateProperty(myObject, nameof(PropertyFoo), PropertyFoo, errors, result => result.Trim());
    

    【讨论】:

    • 不幸的是,我们目前没有使用 C# 6,但 nameof 绝对是我需要的:/
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-30
    • 1970-01-01
    • 1970-01-01
    • 2010-09-09
    • 1970-01-01
    • 1970-01-01
    • 2017-06-14
    相关资源
    最近更新 更多