加快编译动态 IQueryable where 表达式

Speed up compilation of a dynamic IQueryable where expression

在下面的代码中,我正在构建一个动态的 IQueryable where 子句。第二行需要 100 毫秒来执行。除了缓存还有加速的方法吗?

var lambda = Expression.Lambda<Func<T, bool>>(expression, _parameter);
var whereQuery = queryable.Where(lambda);

其中 expression 是 BinaryExpression,_parameter 是 MemberExpression,queryable 是 IQueryable。

编辑

这是完整的代码。它是一个 class,接受一个字符串并将其转换为 lambda 表达式。

public class StringToWhereConverter<T>
    {
        abstract class ClauseElement { }

        class Clause : ClauseElement
        {
            public string FieldName { get; set; }
            public string Operator { get; set; }
            public string Value { get; set; }

            public override string ToString()
            {
                return FieldName + " " + Operator + " " + Value;
            }
        }

        class Connector : ClauseElement
        {
            public string AndOrOr { get; set; }
        }

        class ExpressionClause : ClauseElement
        {
            public BinaryExpression BinaryExpression { get; set; }
        }

        private ParameterExpression _parameter;
        private Dictionary<string, PropertyInfo> _propertyLookup;

        public StringToWhereConverter()
        {
            _parameter = Expression.Parameter(typeof(T), "e");
            PropertyInfo[] propertyInfos = typeof(T).GetProperties();
            _propertyLookup = propertyInfos.ToDictionary(p => p.Name.ToLower());
        }

        public IQueryable<T> WhereQuery(IQueryable<T> queryable, string whereStatement)
        {
            if (string.IsNullOrWhiteSpace(whereStatement))
                return queryable;
            var where = whereStatement.Trim().ToLower();
            var words = SeparateBySpaces(where);
            var clauses = CreateIntoClauses(words);
            var expressions = CreateIntoExpressions(clauses);
            var expression = CreateIntoSingleExpression(expressions);
            var lambda = Expression.Lambda<Func<T, bool>>(expression, _parameter);

            var watch = Stopwatch.StartNew();
            var whereQuery = queryable.Where(lambda);
            watch.Stop();
            var elapsedMs = watch.ElapsedMilliseconds;

            return whereQuery;
        }

        private List<string> SeparateBySpaces(string clause)
        {
            var words = clause.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
            words.ForEach(w => w.Trim());

            if (IsWordWhere(words[0]))
                words.RemoveAt(0);

            List<string> clearerWords = new List<string>();
            foreach (var word in words)
            {


                if (IsWordValidField(word))
                {
                    clearerWords.Add(word);
                    continue;
                }

                if (IsWordContainingOperator(word))
                {
                    var clearerWord = word;
                    var indexOfOperator = clearerWord.IndexOfAny(new char[] { '>', '<', '=', '!' });
                    if (indexOfOperator != 0) // if not at beginning, add space in front
                    {
                        clearerWord = clearerWord.Insert(indexOfOperator, " ");
                        indexOfOperator++;
                    }

                    // test soemthing like feld>>>value or field >= >value
                    if (indexOfOperator + 1 != clearerWord.Length) // if not at end, add space depending
                    {
                        int indexOfLastOperator = indexOfOperator + 1;

                        var isCharAnOperator = true;
                        do
                        {

                            isCharAnOperator = indexOfLastOperator != clearerWord.Length && IsWordContainingOperator(clearerWord[indexOfLastOperator].ToString());
                            if (isCharAnOperator)
                            {
                                indexOfLastOperator++;
                            }
                            else
                            {
                                if (indexOfLastOperator == clearerWord.Length) continue;
                                clearerWord = clearerWord.Insert(indexOfLastOperator, " "); // add a space after the operator
                            }
                        } while (isCharAnOperator);


                    }
                    var clearWordsWithOperator = clearerWord.Split(' ').ToList();
                    clearWordsWithOperator.ForEach(c => clearerWords.Add(c));
                }
            }

            return clearerWords;
        }

        private List<ClauseElement> CreateIntoClauses(List<string> words)
        {
            var originalWords = new List<string>(words);
            var clauseElements = new List<ClauseElement>();
            var dontExitLoop = true;
            do
            {
                var subclause = words.Take(3).ToList();
                if (subclause.Count != 3)
                {
                    var message = "A syntax error occurred. The phrase '" + string.Join(" ", originalWords) + "' is an invalid where clause.";
                    throw new SyntaxException(message);
                }

                bool isValid = IsWordValidField(subclause[0])
                        && IsWordValidOperator(subclause[1]);

                if (isValid)
                {
                    var clauseelement = new Clause()
                    {
                        FieldName = subclause[0],
                        Operator = subclause[1],
                        Value = subclause[2],
                    };
                    clauseElements.Add(clauseelement);
                }
                else
                {
                    var message = "A syntax error occurred. The phrase '" + string.Join(" ", words) + "' is an invalid where clause.";
                    throw new SyntaxException(message);
                }

                words.RemoveRange(0, 3);

                if (words.Count != 0)
                {
                    if (IsWordValidConnector(words[0]))
                    {
                        clauseElements.Add(new Connector() { AndOrOr = words[0] });
                        words.RemoveAt(0);
                    }
                    else
                    {
                        var message = "Expected 'and' or 'or' in the clause instead of '" + words[0] +"'";
                        throw new SyntaxException(message);
                    }
                }
                else
                {
                    dontExitLoop = false;
                }

            } while (dontExitLoop);

            return clauseElements;
        }

        private List<ClauseElement> CreateIntoExpressions(List<ClauseElement> clauses)
        {
            var expressions = new List<ClauseElement>();

            foreach(var clause in clauses)
            {
                if (clause is Clause)
                {
                    var cl = clause as Clause;
                    var expressionClause = new ExpressionClause();
                    expressionClause.BinaryExpression = ConvertClauseToExpression(cl, _parameter);
                    expressions.Add(expressionClause);
                }
                else
                {
                    expressions.Add(clause);
                }
            }

            return expressions;
        }

        private BinaryExpression CreateIntoSingleExpression(List<ClauseElement> expressionClauses)
        {
            if (expressionClauses.Count <= 2)
                return (expressionClauses[0] as ExpressionClause).BinaryExpression;

            BinaryExpression expression;
            var dontExitLoop = true;
            do
            {
                var sub = expressionClauses.Take(3).ToList();
                var comparer = (sub[1] as Connector).AndOrOr;
                expression = (sub[0] as ExpressionClause).BinaryExpression;
                var right = (sub[2] as ExpressionClause).BinaryExpression;
                expression = CreateBinaryExpression(comparer, expression, right);

                expressionClauses.RemoveRange(0, 3);
                expressionClauses.Insert(0, new ExpressionClause() { BinaryExpression = expression });
                dontExitLoop = expressionClauses.Count > 1;
            } while (dontExitLoop);

            return expression as BinaryExpression;
        }

        private BinaryExpression ConvertClauseToExpression(Clause clause, Expression parameter)
        {
            var property = TryGetLookupValue(clause.FieldName);
            var convertedValue = Convert.ChangeType(clause.Value, property.PropertyType);
            var accessor = Expression.MakeMemberAccess(parameter, property);
            var constant = Expression.Constant(convertedValue);
            var comparison = CreateBinaryExpression(clause.Operator, accessor, constant);
            return comparison;
        }

        private BinaryExpression CreateBinaryExpression(string comparer, Expression left, Expression right)
        {
            switch (comparer)
            {
                case ">":
                    return Expression.GreaterThan(left, right);
                case "<":
                    return Expression.LessThan(left, right);
                case ">=":
                    return Expression.GreaterThanOrEqual(left, right);
                case "<=":
                    return Expression.LessThanOrEqual(left, right);
                case "=":
                    return Expression.Equal(left, right);
                case "<>":
                case "!=":
                    return Expression.NotEqual(left, right);
                case "and":
                    return Expression.AndAlso(left, right);
                case "or":
                    return Expression.OrElse(left, right);
                default:
                    throw new Exception();
            }
        }

        private bool IsWordWhere(string word)
        {
            return word == "where";
        }

        private bool IsWordContainingOperator(string word)
        {
            return Regex.IsMatch(word, "<|>|=|!");
        }

        private bool IsWordValidField(string word)
        {
            return !Regex.IsMatch(word, "<|>|=|!");
        }

        private bool IsWordValidOperator(string word)
        {
            var isValid = word == "="
                || word == ">"
                || word == "<"
                || word == ">="
                || word == "<="
                || word == "!="
                || word == "<>";
            return isValid;
        }

        private bool IsWordValidConnector(string word)
        {
            var lower = word.ToLower();
            var IsValid = lower == "and"
                || lower == "or";
            return IsValid;
        }

        private PropertyInfo TryGetLookupValue(string key)
        {
            if (_propertyLookup.ContainsKey(key))
            {
                return _propertyLookup[key];
            }
            else
            {
                var message = "A syntax error occurred. The field '" + key + "' is an invalid field name.";
                throw new SyntaxException(message);
            }
        }
    }

然后它可以像这样与 IQueryable 一起使用:

string clause = "invoiceid = 2 ";
var db = new ApLineContext();
var stringToWhereConverter = new StringToWhereConverter<ApLine>();
var query = stringToWhereConverter.WhereQuery(db.ApLines, clause);
var results = query.ToList();

编辑 2 - 分析器

如果您需要查看更多详细信息,我可以添加屏幕截图。我只打开树,直到我在 1 次点击时发现问题。

分析显示大部分时间花在了名为 get_Provider 的事情上。我的猜测是这是Entity Framework一次性初始化。您的探查器软件似乎有问题,因为它无法解析所有程序集名称。

放弃第一个计时测试,然后再测量 1000000 次迭代。这应该为您提供该段代码的稳态性能。