缓存编译的表达式委托

时间:2016-03-09 18:05:06

标签: c# lambda expression-trees

Backstory:在整个项目中,我们不断发现自己使用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, PC1, Property1C1, Property2C2, Property1等有许多不同的可能排列,因此我无法缓存已编译的表达式并执行它在下一个电话中,除非TP属于同一类型,否则很少发生。

我可以通过将Expression<Func<T, P>> member更改为Expression<Func<T, object>> member来优化此功能,以便现在每个T类型(即C1,{{{}}只需缓存一次表达式1}}等等。)

我想知道是否有人有更好的方法可以做到这一点,或者我是否想要超过工程师&#34;问题?对于涉及一遍又一遍地传递魔法弦的情况,是否存在共同模式?

3 个答案:

答案 0 :(得分:1)

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

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

答案 1 :(得分:1)

另一个选择:使用反射,而不是使用C#6功能或编译。现在,您正在为反射性能下降(可能更少)进行编译性能降低。

first_pistol

答案 2 :(得分:1)

嗯,你是对的,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约束,从而允许验证数字,日期等值,但无论如何。