将查询字符串解析为动态 sql (LINQ)

问题描述

我不常来这里问事情,所以我希望这个问题被允许。

我被分配了一项任务来实现 REST 服务,其中 GET 端点基本上可以作为 OData 服务工作。但不完全是。我试图说服项目所有者,而不是直接实施 OData 服务是更好的解决方案,但我被拒绝了。

我已经尝试过使用蛮力字符串解析和非常繁琐的 switch 语句,但它很混乱。我想知道对于这种类型的事情有什么更好的方法

具体内容如下:

GET /store?query=<expr>
    assume <expr> is URLEncoded

EQUAL(field,val)
    field = table field name
    val = can be either a string or an int depending on the field
    
AND(expr1,expr2)
    expr1/exprr2 = any of valid functions,such as EQUAL(),NOT(),GREATER_THAN(),and LESS_THAN()
    
OR(expr1,and LESS_THAN()
    
NOT(expr)
    expr1 = any of valid functions,and LESS_THAN()
    
GREATER_THAN(field,val)
    field = table field name (must be an int field)
    val = int value to look for 
    
LESS_THAN(field,val)
    field = table field name (must be an int field)
    val = int value to look for 

示例

假设数据本身有一个 List<StoreData>

public class StoreData 
{
    [required]
    public string id { get; set; }
    [required]
    public string title { get; set; }
    [required]
    public string content { get; set; }
    [required]
    public int views { get; set; }
    [required]
    public int timestamp { set; get; }
}
GET /store?query=EQUAL(id,"100")                                   
  -> should fail,improper parameter type

GET /store?query=EQUAL(id,100)                                     
  -> should be successful
 
GET /store?query=EQUAL(views,5)                                    
  -> should be successful

GET /store?query=EQUAL(views,"190)                                 
  -> should fail,improper parameter type

GET /store?query=GREATER_THAN(views,500)                           
  -> should be successful

GET /store?query=GREATER_THAN(views,"500")                         
  -> should fail,improper parameter type

GET /store?query=AND(EQUAL(id,"666"),EQUAL(views,100))             
  -> should be successful

GET /store?query=AND(NOT(EQUAL(id,"666")),100))         
  -> should be successful

GET /store?query=AND(EQUAL(id,GREATER_THAN(views,0))         
  -> should be successful

GET /store?query=AND(GREATER_THAN(views,"100"),EQUAL(id,"666"))    
  -> should fail,improper parameter type

GET /store?query=OR(EQUAL(id,"666"))                
  -> should be successful

GET /store?query=OR(EQUAL(id,100),66))           
  -> should be successful

GET /store?query=NOT(EQUAL(id,"666"))                               
  -> should be successful

GET /store?query=EQUAL(title,"Title 756")                          
  -> should be successful

GET /store?query=EQUAL(TITLE,"TITLE 756")                          
  -> should be successful,just ensuring it's case insensitive

GET /store?query=EQUAL(blah,100)                                   
  -> should fail,unrecognized field

除了蛮力,我的想法是某种词法解析器,但我对如何进行几乎一无所知。

有人问我已经做过的蛮力解析,所以这里是...主要的解析逻辑

    // Ideally a whole expression parsing engine should be built,but I'm on a limited time frame
    // After working all night,and struggling on the OR,I added DynamicLinq thru NuGet,which made the OR
    // sooooooooo much easier.  I really wish I had to time to re-write this entire thing using that DynamicLinq
    // for everything... but on a time constraint here.  I wonder if the "customer" would accept a delay?  :)
    public List<StoreData> GetData(string sQueryString)
    {
        int iPos1 = sQueryString.IndexOf('(');
        int iPos2 = sQueryString.LastIndexOf(')');
        string firstAction = sQueryString.Substring(0,iPos1);

        // ideally if this were a database,this would be handled completely different with a EF dbContext
        // but this list is relatively small. In a production app,monitor this performance.
        // we build is as an IQueryable so we can add to the WHERE condition before running the sql
        var theData = Startup.MockData.AsQueryable();

        // short cut for these first few types...
        // for functions that are not nested,_params.Length should equal 2
        // nested functions will produce _params.Length would be more,depending on the nesting level
        string[] _params = sQueryString.Substring(iPos1 + 1,(iPos2 - iPos1) - 1).Split(',');

        // since we're starting with the easy direct field functions first,we kNow there'll be 2 elements in _params
        string fieldName = _params[0].Trim();
        string fieldValue = _params[1].Trim();
        int iVal = 0;

        // EQUAL(field,val)
        // AND(expr1,expr2)
        //      AND(EQUAL(field,val),EQUAL(field,val))
        //      AND(EQUAL(field,NOT(EQUAL(field,val)))
        //      AND(EQUAL(field,GREATER_THAN(field,val))
        // OR(expr1,expr2)
        //      OR(EQUAL(field,val))
        //      OR(EQUAL(field,val)))
        //      OR(EQUAL(field,val))
        // NOT(expr)
        // GREATER_THAN(field,val)
        // LESS_THAN(field,val)
        //
        // functions with expression parameters (expr<x>) can be any of the functions directly operating on fields

        switch (firstAction.toupper())
        {
            // these first 3 should be the easiest to deal with initially since the operate
            // directly on fields and values as opposed to an expression
            case "EQUAL":
                theData = HandleEQUAL(theData,fieldName,fieldValue);
                break;

            case "GREATER_THAN":
            case "LESS_THAN":
                iVal = -1;
                if(!int.TryParse(fieldValue.Trim(),out iVal))
                {
                    throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                }

                if (iVal == -1)
                {
                }

                if(!_intProperties.Any(x => x.toupper() == fieldName.toupper()))
                {
                    throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                }

                if(firstAction.toupper() == "GREATER_THAN")
                {
                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) > iVal);
                }
                else if (firstAction.toupper() == "LESS_THAN")
                {
                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) < iVal);
                }
                break;

            // I'm not sure how many different permutations there can be with this command
            // but for Now only handling EQUAL as the parameter.  It doesn't make much sense to
            // handle a NOT(GREATER_THAN()) or NOT(LESS_THAN()) because they have mutually exclusive
            // selection commands already.  a NOT(GREATER_THAN()) is basically the same as LESS_THAN(),// and a NOT(LESS_THAN()) is a GREATER_THAN()
            //
            // do I need to worry about a NOT(AND(EQUAL(id,"1"),NOT(EQUAL(views,666))) ?
            // or how about NOT(NOT(EQUAL(id,"100")) ??
            case "NOT":
                fieldName = fieldName.Replace("EQUAL(","",StringComparison.OrdinalIgnoreCase).Trim();
                fieldValue = fieldValue.Replace(")","").Trim();
                theData = theData.Where(x => ((string)x.GetValueByName(fieldName)) != fieldValue.Replace("\"",""));
                break;

            // these are the painful ones,with many permutations
            // these 2 can be combined in many different statements,using a mixture of any of the top level commands
            // AND(EQUAL(id,100)) - shouldn't be too difficult
            // AND(NOT(EQUAL(id,100)) - ARGH!!! 
            // AND(EQUAL(id,100)) - 
            case "AND":
                #region Handles EQUAL() and NOT(EQUAL())
                // 1st expression in the AND(expr1,expr2) function call
                if (_params[0].Contains("EQUAL(",StringComparison.OrdinalIgnoreCase))
                { 
                    fieldName = _params[0].Replace("EQUAL(",StringComparison.OrdinalIgnoreCase).Trim();
                    fieldValue = _params[1].Replace(")","").Trim();

                    if(_stringProperties.Any(x => x.toupper() == fieldName.toupper()))
                    {
                        if (_params[0].Contains("NOT(",StringComparison.OrdinalIgnoreCase))
                        {
                            theData = HandleNOT_EQUAL(theData,fieldValue);
                        }
                        else
                        {
                            theData = HandleEQUAL(theData,fieldValue);
                        }
                    }
                    else if (_intProperties.Any(x => x.toupper() == fieldName.toupper()))
                    {
                        iVal = -1;
                        if(!Int32.TryParse(fieldValue.Trim(),out iVal))
                        {
                            throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                        }

                        if (_params[0].Contains("NOT(",StringComparison.OrdinalIgnoreCase))
                        {
                            theData = theData.Where(x => ((int) x.GetValueByName(fieldName)) != iVal);
                        }
                        else
                        {
                            theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) == iVal);
                        }
                    }

                }

                // 2nd expression in the AND(expr1,expr2) function call
                if (_params[2].Contains("EQUAL(",StringComparison.OrdinalIgnoreCase))
                {
                    fieldName = _params[2].Replace("EQUAL(",StringComparison.OrdinalIgnoreCase).Trim();
                    fieldValue = _params[3].Replace(")","").Trim();

                    if(_stringProperties.Any(x => x.toupper() == fieldName.toupper()))
                    {
                        if (_params[2].Contains("NOT(",fieldValue);
                        }
                    }
                    else if (_intProperties.Any(x => x.toupper() == fieldName.toupper()))
                    {
                        iVal = -1;
                        if (!Int32.TryParse(fieldValue.Trim(),StringComparison.OrdinalIgnoreCase))
                        {
                            theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) != iVal);
                        }
                        else
                        {
                            theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) == iVal);
                        }
                    }
                }
                #endregion

                #region Handles GREATER_THAN() and LESS_THAN()
                if(_params[0].Contains("GREATER_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    fieldName = _params[0].Replace("GREATER_THAN(","").Trim();

                    iVal = -1;
                    if (!Int32.TryParse(fieldValue.Trim(),out iVal))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    if (!_intProperties.Any(x => x.toupper() == (fieldName.toupper())))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) > iVal);
                }

                if (_params[2].Contains("GREATER_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    fieldName = _params[2].Replace("GREATER_THAN(",out iVal))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    if (!_intProperties.Any(x => x.toupper() == (fieldName.toupper())))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) > iVal);
                }

                if (_params[0].Contains("LESS_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    fieldName = _params[0].Replace("LESS_THAN(","").Trim();

                    iVal = -1;

                    if (!Int32.TryParse(fieldValue.Trim(),out iVal))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    if (!_intProperties.Any(x => x.toupper() == (fieldName.toupper())))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) < iVal);
                }

                if (_params[2].Contains("LESS_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    fieldName = _params[2].Replace("LESS_THAN(",out iVal))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    if (!_intProperties.Any(x => x.toupper() == (fieldName.toupper())))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }

                    theData = theData.Where(x => ((int)x.GetValueByName(fieldName)) < iVal);
                }
                #endregion

                break;

            // the most challenging combination because adding onto an IQueryable is an AND,there is no alternative for an OR
            case "OR":
                // this one is gonna take some brute force
                // so I added Dynaimc LINQ via NuGet,which helped tremendously with this OR condition
                // serIoUsly debating re-writing this entire thing using the functionality of the dynamic LINQ
                // while the code I wrote last night is far from elegant,it's working for the time being.  If
                // this were indeed a legitimate customer requested project,I would go back through and clean it
                // up significantly,but time is of the essence for the turn in on this project.
                int iVal1 = -1;
                int iVal2 = -1;

                ORCondition exprConditions = new ORCondition();

                #region Handles EQUAL() and NOT(EQUAL())
                if (_params[0].Contains("EQUAL(",StringComparison.OrdinalIgnoreCase))
                { 
                    exprConditions.Field1 = _params[0].Replace("EQUAL(",StringComparison.OrdinalIgnoreCase).Trim();
                    exprConditions.Value1 = _params[1].Replace(")","").Trim();
                    exprConditions.Operator1 = "==";

                    if(_stringProperties.Any(x => x.toupper() == exprConditions.Field1.toupper()) && !exprConditions.Value1.Contains("\""))
                    {
                        throw new Exception($"Malformed Query {exprConditions.Field1} parameter must be a string type");
                    }

                    if(_intProperties.Any(x => x.toupper() == exprConditions.Field1.toupper()) && exprConditions.Value1.Contains("\""))
                    {
                        throw new Exception($"Malformed Query {exprConditions.Field1} parameter must be an inttype");
                    }                            

                    if (_params[0].Contains("(NOT(",StringComparison.OrdinalIgnoreCase))
                    {
                        exprConditions.Not1 = true;
                        exprConditions.Operator1 = "!=";
                    }
                }

                if(_params[2].Contains("EQUAL(",StringComparison.OrdinalIgnoreCase))
                {
                    exprConditions.Field2 = _params[2].Replace("EQUAL(",StringComparison.OrdinalIgnoreCase).Trim();
                    exprConditions.Value2 = _params[3].Replace(")","").Trim();
                    exprConditions.Operator2 = "==";

                    if (_stringProperties.Any(x => x.toupper() == exprConditions.Field2.toupper()) && !exprConditions.Value2.Contains("\""))
                    {
                        throw new Exception($"Malformed Query {exprConditions.Field2} parameter must be a string type");
                    }

                    if (_intProperties.Any(x => x.toupper() == exprConditions.Field2.toupper()) && exprConditions.Value2.Contains("\""))
                    {
                        throw new Exception($"Malformed Query {exprConditions.Field2} parameter must be an inttype");
                    }

                    if (_params[2].Contains("(NOT(",StringComparison.OrdinalIgnoreCase))
                    {
                        exprConditions.Not2 = true;
                        exprConditions.Operator1 = "!=";
                    }
                }
                #endregion

                #region Handles GREATER_THAN() and LESS_THAN()
                if(_params[0].Contains("GREATER_THAN)",StringComparison.OrdinalIgnoreCase))
                {
                    exprConditions.Field1 = _params[0].Replace("GREATER_THAN(","").Trim();
                    exprConditions.Operator1 = ">";

                    // technically,there shouldn'ty be NOT(GREATER_THAN()) because that would
                    // pretty much be the same as LESS_THAN()
                    //if (_params[0].Contains("(NOT(",StringComparison.OrdinalIgnoreCase))
                    //{
                    //    exprConditions.Not1 = true;
                    //}

                    iVal1 = -1;
                    if (!Int32.TryParse(exprConditions.Value1,out iVal1))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }
                }

                if (_params[2].Contains("GREATER_THAN",StringComparison.OrdinalIgnoreCase))
                {
                    exprConditions.Field2 = _params[2].Replace("GREATER_THAN(","").Trim();
                    exprConditions.Operator2 = ">";

                    iVal2 = -1;
                    if (!Int32.TryParse(exprConditions.Value2,out iVal2))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }
                }

                if (_params[0].Contains("LESS_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    exprConditions.Field1 = _params[0].Replace("LESS_THAN(","").Trim();
                    exprConditions.Operator1 = "<";

                    iVal1 = -1;
                    if (!Int32.TryParse(exprConditions.Value1,out iVal1))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }
                }

                if (_params[2].Contains("LESS_THAN(",StringComparison.OrdinalIgnoreCase))
                {
                    exprConditions.Field2 = _params[2].Replace("LESS_THAN(","").Trim();
                    exprConditions.Operator2 = "<";

                    iVal2 = -1;
                    if (!Int32.TryParse(exprConditions.Value2,out iVal2))
                    {
                        throw new Exception($"Malformed Query Parameter: {fieldName} parameter must be an int type");
                    }
                }
                #endregion

                exprConditions.Value1 = exprConditions.Value1.Replace("\"","");
                exprConditions.Value2 = exprConditions.Value2.Replace("\"","");

                // we should have everything parsed and the ORCondition populated with the appropriate
                // flags to help build the LINQ statement
                #region let's try this thing
                string dQuery = $"{exprConditions.Field1} {exprConditions.Operator1} ";

                if(_stringProperties.Any(x => x.toupper() == exprConditions.Field1.toupper()))
                {
                    dQuery += $"\"{exprConditions.Value1}\" OR ";
                }
                else if (_intProperties.Any(x => x.toupper() == exprConditions.Field1.toupper()))
                {
                    dQuery += $"{exprConditions.Value1} OR ";
                }

                dQuery += $"{exprConditions.Field2} {exprConditions.Operator2} ";

                if(_stringProperties.Any(x => x.toupper() == exprConditions.Field2.toupper()))
                {
                    dQuery += $"\"{exprConditions.Value2}\"";
                }
                else if(_intProperties.Any(x => x.toupper() == exprConditions.Field2.toupper()))
                {
                    dQuery += $"{exprConditions.Value2}";
                }

                theData = theData.Where(dQuery);
                #endregion

                break;

            default:
                throw new Exception($"Malformed Query Parameter: {firstAction} not a recognized function");
        }

        return theData.ToList();
    }

解决方法

虽然我不是解析器和创建领域特定语言的专家(除了学习时的小介绍),我绝对认为您正在寻找的解决方案是一个合适的解析器,正如评论中所建议的那样。

>

Sprache,正如有人已经建议的那样,是我偶然发现的应该能够为您完成这项工作的图书馆的一个例子。大多数使用组合子/运算符的 Sprache 示例都显示了操作数之间的运算符。同样,Sprache 辅助函数 ChainOperator 似乎希望运算符出现在我没有找到简单替代方法的操作数之间。这使得找到一个工作递归模式有点困难,但仍然是一个有趣的挑战。

由于您似乎正在研究类似 List 的结构,我希望使用表达式作为解析结果是合适的。您可以像在基于 Linq 的方法中所做的那样使用这些。所以,我想出的东西(来自下面链接的不同来源的灵感)类似于我在下面的内容。

为了让我更容易理解如何在 Sprache 中构建定义,我开始尝试在 EBNF 中编写语法定义(稍微简化):

Term        := Comparer | Combinator

Combinator  := OperatorId,'(',Term,',')'
OperatorId  := 'AND' | 'OR'

Comparer    := ComparerId,Word,Value,')'
ComparerId  := 'EQUAL' | 'GREATER_THAN' | 'LESS_THAN'

Value       := Number | String
Number      := { Digit };
String      := Quote,Quote;
Word        := { Letter }
Letter      := 'a' | 'b' | 'c' ...;
Digit       := '1' | '2' | '3' ...;
Quote       := '"'

从各地汲取灵感,这让我在代码中找到了这种结构:

public static class QueryGrammar<T>
{
    // We're parsing the query and translating it into an expression in the form
    // of a lambda function taking a parameter of type T as input
    static readonly ParameterExpression Param = Expression.Parameter(typeof(T));
    
    // The only public member
    public static Expression<Func<T,bool>> ParseCondition(string input)
    {
        // Parse a Term (recursively) until the end of the input is reached.
        // Convert the output expression body into a lambda function that can
        // be applied to an object of type T returning a bool (predicate)
        return Term.End()
            .Select(body => Expression.Lambda<Func<T,bool>>(body,Param))
            .Parse(input);
    }
    
    // For escaping a character (such as ")
    static Parser<char> EscapingChar => Parse.Char('\\');
    // Pick the character that was escaped
    static Parser<char> EscapedChar =>
        from _ in EscapingChar
        from c in Parse.AnyChar
        select c;
        
    static Parser<char> QuoteChar => Parse.Char('"');
    static Parser<char> NonQuoteChar =>
        Parse.AnyChar.Except(QuoteChar);
    
    // Pick text in-between double qoutes
    static Parser<string> QuotedText =>
        from open in QuoteChar
        from content in EscapedChar.Or(NonQuoteChar).Many().Text()
        from close in QuoteChar
        select content;
    
    // Create an expression that "calls the getter of a property on a object of type T"
    private static Expression GetProperty(string propName)
    {
        var memInfo = typeof(T).GetMember(propName,MemberTypes.Property,BindingFlags.Instance | BindingFlags.Public)
            .FirstOrDefault();
            
        if (memInfo == null)
            throw new ParseException($"Property with name '{propName}' not found on type '{typeof(T)}'");
            
        return Expression.MakeMemberAccess(Param,memInfo);
    }
    
    // Match a word without quotes,in order to match a field/property name
    static Parser<Expression> FieldToken =>
        Parse.Letter.AtLeastOnce().Text().Token()
        .Select(name => GetProperty(name));
    // Match a value. Either an integer or a quoted string
    static Parser<Expression> ValueToken =>
        (from strNum in Parse.Number
        select Expression.Constant(int.Parse(strNum)))
        .Or(QuotedText.Select(t => Expression.Constant(t)))
        .Token();
        
    // Common parser for all terms
    static Parser<Expression> Term =>
        EqualTerm
        .Or(GreaterThanTerm)
        .Or(OrTerm)
        .Or(AndTerm);
    
    static Parser<Expression> EqualTerm =>
        (from tuple in Comparer("EQUAL")
         select Expression.Equal(tuple.Field,tuple.Value))
        .Token();
    static Parser<Expression> GreaterThanTerm =>
        (from tuple in Comparer("GREATER_THAN")
         select Expression.GreaterThan(tuple.Field,tuple.Value))
        .Token();
        
    // Generally the comparer terms consist of:
    // COMPARER_NAME + ( + FIELD_NAME +,+ VALUE + )
    static Parser<(Expression Field,Expression Value)> Comparer(string comparison) =>
        (from startsWith in Parse.String(comparison).Text().Token()
         from openParen in Parse.Char('(')
         from field in FieldToken
         from comma in Parse.Char(',')
         from value in ValueToken
         from closeParen in Parse.Char(')')
         select (field,value))
        .Token();
        
        
    static Parser<Expression> AndTerm =>
        (from tuple in Combinator("AND")
         select Expression.AndAlso(tuple.Left,tuple.Right))
        .Token();
    static Parser<Expression> OrTerm =>
        (from tuple in Combinator("OR")
         select Expression.OrElse(tuple.Left,tuple.Right))
        .Token();
        
    // Generally combinators consist of:
    // COMBINATOR_NAME + ( + TERM +,+ TERM + )
    // A term can be either a comparer of another combinator as seem above
    static Parser<(Expression Left,Expression Right)> Combinator(string combination) =>
        (from startsWith in Parse.String(combination).Text().Token()
         from openParen in Parse.Char('(')
         from lTerm in Term
         from comma in Parse.Char(',')
         from rTerm in Term
         from closeParen in Parse.Char(')')
         select (lTerm,rTerm))
        .Token();
}

然后按如下方式使用它(当然您可以从查询中删除所有换行符和空格):

var expr = QueryGrammar<StoreData>.ParseCondition(@"
    OR(
        AND(
            EQUAL(title,""Foo""),AND(
                GREATER_THAN(views,50),EQUAL(timestamp,123456789)
            )
        ),EQUAL(id,""123"")
    )");
Console.WriteLine(expr);
var func = expr.Compile();
var result = func(new StoreData
            {
                id = "321",title = "Foo",views = 51,timestamp = 123456789
            });
Console.WriteLine(result);

可以在此处找到工作示例:https://dotnetfiddle.net/3STNYm

现在你只需要稍微扩展一下。例如。我没有写 NOTLESS_THAN 项,您可能需要检查是否正确处理了空格(字符串值的内部和外部)以及您能想到的其他极端情况(否定数字、高于 int.MaxValue 的数字)和错误处理/报告(如果查询无效,您是否需要返回正确的消息?)。幸运的是,write unit tests 这个库似乎很容易检查解析是否按预期工作。

一点功劳是应该的:

最后一点:如果您的 PO 还不是很明显,那么可能值得让他们知道,与使用您建议的 OData 等一些现成工具相比,此解决方案并不便宜。解析器代码(即使使用像 Sprache 这样的库)很复杂,难以维护,难以推理(至少对于非解析器专家来说),需要大量测试,并且更有可能留下难以发现的错误和漏洞与流行且维护良好的工具相比,为时已晚。另一方面,它为您提供了更多的自由和未来扩展的可能性。

,

我喜欢@Xerillio 的回答,但这里有另一种方法,随着需求的发展,它可能更易于维护。

以下是我处理此类问题的方法:

首先弄清楚语法,恕我直言,越简单越好。这是我想出来的。

ex := op '(' pr ')'             //expression
pr := id ',' lt | ex (',' ex)*  //parameter (* means zero or more)
op := string                    //operator
id := string                    //id
lt := quoted-string | number    //literal

然后根据该语法创建一个解析树(Parser),应尽可能地反映该语法。

然后遍历该解析树以计算结果 (Calc)。

注意:Parser 用户 Sprache 和 Calc 使用递归非常简单。

警告 Emptor,可能有问题...

https://dotnetfiddle.net/Widget/pgsbjb

using System;
using Sprache;
using System.Linq;
using System.Collections.Generic;

namespace dotnet_parser
{
    class Program
    {
        /*
            ex := op '(' pr ')'             //expression
            pr := id ',' ex)*  //parameter (* means zero or more)
            op := string                    //operator
            id := string                    //id
            lt := quoted-string | number    //literal
        */

        static void Main(string[] args)
        {
            var ex = Parser.getEx().Parse(@"
                                            OR(
                                                AND(
                                                    EQUAL(title,AND(
                                                        GREATER_THAN(views,123456789)
                                                    )
                                                ),""123"")
                                            )");
            
            var sd =
                new StoreData
                {
                    id = "321",views = 49,timestamp = 123456789
                };

            var res = Calc.Run(ex,sd);

            Console.WriteLine($"result {res}");
        }

        public class StoreData 
        {
            public string id { get; set; }
            public string title { get; set; }
            public string content { get; set; }
            public int views { get; set; }
            public int timestamp { set; get; }
        }

        public class Calc {
            public static bool Run<T>(Parser.Ex ex,T t) {

                switch(ex.Op) {
                    case "NOT":
                    {
                        if(ex.Pr is Parser.PrEx exPr && exPr.Ex.Length == 1) {
                            return Run(exPr.Ex[0],t);
                        }
                        else {
                            throw new Exception($"invalid parameters to {ex.Op}");
                        }
                    }
                    case "AND":
                    case "OR":
                    {
                        if(ex.Pr is Parser.PrEx exPr && exPr.Ex.Length == 2) {
                            var l = Run(exPr.Ex[0],t);
                            var r = Run(exPr.Ex[1],t);
                            switch(ex.Op) {
                                case "AND": return l && r;
                                case "OR" : return l || r;
                                default:
                                    throw new Exception();
                            }
                        }
                        else {
                            throw new Exception($"invalid parameters to {ex.Op}");
                        }
                    }
                    case "EQUAL":
                    case "GREATER_THAN":
                    case "LESS_THAN":
                    {
                        if(ex.Pr is Parser.PrIdLt exIdLt) {
                            var tt = typeof(T);
                            var p = tt.GetProperty(exIdLt.Id);

                            if(p == null) throw new Exception($"no property {exIdLt.Id} on {tt.Name}");


                            if(p.PropertyType == typeof(string) && exIdLt.Lt is Parser.LtQuotedString ltQuotedString){
                                var pval = p.GetValue(t) as string;
                                switch(ex.Op){
                                    case "EQUAL":
                                        return pval == ltQuotedString.Val;
                                    default:
                                        throw new Exception($"{ex.Op} invalid operator for string");
                                }
                            }
                            else if(p.PropertyType == typeof(int) && exIdLt.Lt is Parser.LtNumber ltNumber){
                                var pval = (int)p.GetValue(t);

                                int lval;
                                if(!int.TryParse(ltNumber.Val,out lval)) throw new Exception($"{ex.Op} {exIdLt.Id} {ltNumber.Val} is not a number");

                                switch(ex.Op){
                                    case "EQUAL"       : return pval == lval;
                                    case "GREATER_THAN": return pval >  lval;
                                    case "LESS_THAN"   : return pval <  lval;
                                    default:
                                        throw new Exception($"{ex.Op} invalid operator for string");
                                }
                            }
                            else {
                                throw new Exception($"{ex.Op} {exIdLt.Id} invalid type");
                            }
                        }
                        else {
                            throw new Exception($"invalid parameters to {ex.Op}");
                        }
                    }
                    default:
                        throw new Exception($"{ex.Op} unknown operator");
                }
            }
        }

        public class Parser {
            public class Ex {
                public string Op { get; set; }
                public Pr Pr { get; set; }
            }

            public interface Pr { }
            public class PrIdLt : Pr { 
                public string Id { get; set; }
                public Lt Lt { get; set; }
            }
            public class PrEx : Pr {
                public Ex[] Ex { get; set; }
            }

            public interface Lt { }
            public class LtQuotedString : Lt { public string Val { get; set; } }
            public class LtNumber       : Lt { public string Val { get; set; } }

            public static Parser<Ex> getEx(){
                return
                    from o in getOp()
                    from p in getPr().Contained(Parse.Char('('),Parse.Char(')')).Token()
                    select new Ex { Op = o,Pr = p };

            }
            public static Parser<Pr> getPr(){
                var a =
                    from i in getId()
                    from c in Parse.Char(',').Token()
                    from l in getLt()
                    select new PrIdLt { Id = i,Lt = l };

                var b =
                    from ex in Parse.Ref(() => getEx())
                    from em in Parse.Char(',').Token().Then(_ => Parse.Ref(() => getEx())).Many()
                    select new PrEx { Ex = em.Prepend(ex).ToArray() };

                return a.Or<Pr>(b);
            }
            public static Parser<string> getOp() => Parse.Letter.Or(Parse.Char('_')).AtLeastOnce().Text().Token();
            public static Parser<string> getId() => Parse.Identifier(Parse.Letter,Parse.LetterOrDigit).Token();
            public static Parser<Lt> getLt(){
                var quoted_string =
                    from open in Parse.Char('"')
                    from value in Parse.CharExcept('"').Many().Text()
                    from close in Parse.Char('"')
                    select value;

                return
                    quoted_string.Select(a => new LtQuotedString { Val = a })
                    .Or<Lt>(Parse.Number.Select(a => new LtNumber(){ Val = a })).Token();
            }
        }
    }
}