首页 > 解决方案 > 从字符串动态构建 LINQ 查询

问题描述

这是我的场景:

有一个对象集合,其中每个对象都包含一个Dictionary<string, string>. 用户可以从另一个应用程序中为此集合构建一组查询,以通过选择Key中的a Dictionary、诸如>orCONTAINS等​​运算符和 a来获取子集Value。他们还可以平衡括号以创建查询组并选择AND/OR运算符来组合查询。

例如,假设我有一个Car对象集合和Dictionary包含MakeModel和的键Year

我的应用程序以字符串的形式获取这些查询,如下所示:

"((Make = Honda) AND (Model CONTAINS Civic)) || (Year >= 2015)"

这告诉我,从我想要具有和OR键/值的Car汽车的对象集合中Dictionary<Make, Honda><Model, anything that contains "Civic"><Year, greater than or equal to 2015>

因此,我将它们解析出来,并将它们放入一个QueryClass包含 、 和 的三个字符串Key字段OperatorValue。我还跟踪查询之间的运算符,以及它们是否在一组括号中。

目前,我必须QueryClass逐个执行查询,检查前一个运算符是什么,它是否是组的一部分等,并一遍又一遍地组合集合,直到它到达末尾。这很乏味,似乎是一种糟糕的做事方式。如果有办法在这个集合上动态构建这些 LINQ 查询或执行 SQL 语句(这些是必不可少的),那就更好了。

这是我存储解析字符串的查询类:

class QueryClass
{
    public string FieldName { get; set; }
    public string Operator { get; set; }
    public object Value { get; set; }

    public QueryClass(string pInput)
    {
        var returned = RegexHelpers.SplitKeyValue(pInput); //just returns a string like "Make = Honda" into three parts
        if (returned != null)
        {
            FieldName = returned.Item1;
            Operator = returned.Item2;
            Value = returned.Item3;
        }
    }
}

我的解析类很长,所以我不会发布整个内容,但它会返回List<object>每个元素所在的位置:

List<object>这是我在解析字符串后得到的示例:

在此处输入图像描述

然后,我只需遍历每个元素,确定该值是双精度值还是字符串,然后对我的集合执行 LINQ 语句。我正在检查运算符是“AND”还是“OR”(如果它只是一个查询,则没有),它是否是组的一部分,并适当地组合结果。

标签: c#linqsubset

解决方案


这是我将您的查询转换为Func. 由于我不确定您的集合中是什么类型,所以我制作了一个接口来表示具有attributes Dictionary<string, string>并处理它的对象。

基本上我添加了一种方法来QueryClass将其转换为Expression. 它使用帮助字典 string->lambdaExpression为每个运算符构建适当的比较。然后我添加了一个类来将其List<object>转换为Func<IItem,bool>适合 LINQ 的Where过滤器。

public interface IItem {
    Dictionary<string, string> attributes { get; set; }
}

class QueryClass {
    public string FieldName { get; set; }
    public string Operator { get; set; }
    public object Value { get; set; }

    public QueryClass(string pInput) {
        var returned = RegexHelpers.SplitKeyValue(pInput); //just returns a string like "Make = Honda" into three parts
        if (returned != null) {
            FieldName = returned.Item1;
            Operator = returned.Item2;
            Value = returned.Item3;
        }
    }

    static MethodInfo getItemMI = typeof(Dictionary<string, string>).GetMethod("get_Item");
    static Dictionary<string, Func<Expression, Expression, Expression>> opTypes = new Dictionary<string, Func<Expression, Expression, Expression>> {
        { "==", (Expression lhs, Expression rhs) => Expression.MakeBinary(ExpressionType.Equal, lhs, rhs) },
        { ">=", (Expression lhs, Expression rhs) => Expression.MakeBinary(ExpressionType.GreaterThanOrEqual, Expression.Call(lhs, typeof(String).GetMethod("CompareTo", new[] { typeof(string) }), rhs), Expression.Constant(0)) },
        { "CONTAINS",  (Expression lhs, Expression rhs) => Expression.Call(lhs, typeof(String).GetMethod("Contains"), rhs) }
    };
    static MemberInfo attribMI = typeof(IItem).GetMember("attributes")[0];

    public Expression AsExpression(ParameterExpression p) {
        var dictField = Expression.MakeMemberAccess(p, attribMI);
        var lhs = Expression.Call(dictField, getItemMI, Expression.Constant(FieldName));
        var rhs = Expression.Constant(Value);

        if (opTypes.TryGetValue(Operator, out var exprMakerFn))
            return exprMakerFn(lhs, rhs);
        else
            throw new InvalidExpressionException($"Unrecognized operator {Operator}");
    }
}

public class LinqBuilder {
    static Type TItems = typeof(IItem);

    static Expression BuildOneLINQ(object term, ParameterExpression parm) {
        switch (term) {
            case QueryClass qc: // d => d[qc.FieldName] qc.Operator qc.Value
                return qc.AsExpression(parm);
            case List<object> subQuery:
                return BuildLINQ(subQuery, parm);
            default:
                throw new Exception();
        }
    }

    static Expression BuildLINQ(List<object> query, ParameterExpression parm) {
        Expression body = null;
        for (int queryIndex = 0; queryIndex < query.Count; ++queryIndex) {
            var term = query[queryIndex];
            switch (term) {
                case string op:
                    var rhs = BuildOneLINQ(query[++queryIndex], parm);
                    var eop = (op == "AND") ? ExpressionType.AndAlso : ExpressionType.OrElse;
                    body = Expression.MakeBinary(eop, body, rhs);
                    break;
                default:
                    body = BuildOneLINQ(term, parm);
                    break;
            }
        }

        return body;
    }

    public static Func<IItem, bool> BuildLINQ(List<object> query) {
        var parm = Expression.Parameter(TItems, "i");
        return Expression.Lambda<Func<IItem, bool>>(BuildLINQ(query, parm), parm).Compile();
    }
}

一旦你有了这个,你可以传入一个List<object>表达式,然后过滤你的集合。给定一个查询qIItems的集合cs,您可以执行以下操作:

var ans = cs.Where(LinqBuilder.BuildLINQ(q));

推荐阅读