简体   繁体   中英

OData: Do case insensitive comparison using ExpressionVisitor

For example, i have OData query such as these:

  • /Suppliers?$filter=Address/City eq 'city'
  • /Suppliers?$filter=contains(Address/City, 'city')
  • /Suppliers?$filter=endswith(Address/City, 'city')

...

and Address/City only has "City". I still want the query to return that record.

I read this already but it only appear to address contains function. I could easily fix for other functions but eq is more difficult. To make it simpler, i was thinking about just replacing all string const to uppercase by doing this .ToString().ToUpper().

I am having a problem because i can't really access the value.

protected override Expression VisitConstant(ConstantExpression node)
            {
//the node.Value i get here is {value(System.Web.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String])} 
and of type System.Linq.Expressions.ConstantExpression.

How can i either modify the value directly to upper case or add a call to ToString and ToUpper?

The ideal solution would be to handle this on the client side with the built-in tolower and toupper filter functions. That would allow the client to make the choice whether to filter with or without case-sensitivity.

On the server side, current best practice is, unfortunately, to modify the request URI and generate a new request object for the modified URI. See OData V4 modify $filter on server side . There is an open issue in Web API OData for a more elegant query option interception/modification mechanism .

Doing string surgery directly on the request URI is always going to be error-prone. We can improve on the linked answer by taking advantage of the rich object model for filter expressions in the OData Library (ODL). Note that the filter expression on ODataQueryOptions.Filter.FilterClause.Expression is an abstract syntax tree representing the value of $filter .

The code that follows requires namespaces from ODL:

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriBuilder;
using Microsoft.OData.Core.UriParser;
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.TreeNodeKinds;
using Microsoft.OData.Edm;

First, define a helper class for rewriting the various nodes that make up a filter AST. The following class currently only handles BinaryOperatorNode (eg, eq expressions).

public static class FilterExpressionHelper
{
    public static SingleValueNode RewriteAsCaseInsensitive(BinaryOperatorNode node)
    {
        // Handle {Edm.String eq Edm.String}
        if (node.OperatorKind == BinaryOperatorKind.Equal && node.Left.TypeReference.IsString() && node.Right.TypeReference.IsString())
        {
            // Wrap both operands with toupper().
            node = new BinaryOperatorNode(BinaryOperatorKind.Equal,
                new SingleValueFunctionCallNode("toupper", new List<QueryNode> { node.Left }, node.Left.TypeReference),
                new SingleValueFunctionCallNode("toupper", new List<QueryNode> { node.Right }, node.Right.TypeReference));
        }

        return node;
    }

    // Add methods to handle other node types; e.g., SingleValueFunctionCallNode.
}

Next, define a helper to invoke the rewriter and regenerate the query string of the request URI (as modeled by ODataQueryOptions ).

public class ODataUriHelper
{
    public static string RewriteQuery(ODataQueryOptions queryOptions)
    {
        var odataUri = BuildODataUri(queryOptions);
        var uriBuilder = new ODataUriBuilder(ODataUrlConventions.Default, odataUri);
        var uri = uriBuilder.BuildUri();
        // Do not return the leading '?'.
        return uri.Query.Substring(1);
    }

    private static readonly Uri DummyServiceRoot = new Uri("http://localhost");
    private static readonly ODataPath DummyPath = new ODataPath(Enumerable.Empty<ODataPathSegment>());

    private static ODataUri BuildODataUri(ODataQueryOptions queryOptions)
    {
        var uri = new ODataUri();

        uri.ServiceRoot = DummyServiceRoot;
        uri.Path = DummyPath;
        uri.Filter = RewriteFilter(queryOptions.Filter?.FilterClause);
        uri.OrderBy = queryOptions.OrderBy?.OrderByClause;
        uri.SelectAndExpand = queryOptions.SelectExpand?.SelectExpandClause;
        uri.Skip = queryOptions.Skip?.Value;
        uri.Top = queryOptions.Top?.Value;

        return uri;
    }

    private static FilterClause RewriteFilter(FilterClause filterClause)
    {
        if (filterClause != null)
        {
            var filterExpr = filterClause.Expression;
            var binaryExpr = filterExpr as BinaryOperatorNode;

            if (binaryExpr != null)
            {
                filterExpr = FilterExpressionHelper.RewriteAsCaseInsensitive(binaryExpr);
                filterClause = new FilterClause(filterExpr, filterClause.RangeVariable);
            }

            // Add support for other node types here.
        }

        return filterClause;
    }
}

Finally, tie it all together with a custom version of the EnableQuery attribute that conditionally performs rewriting.

public class CustomEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        if (queryOptions.Filter != null)
        {
            var query = ODataUriHelper.RewriteQuery(queryOptions);
            var uri = new UriBuilder(queryOptions.Request.RequestUri) { Query = query };
            var request = new HttpRequestMessage(HttpMethod.Get, uri.Uri);

            queryOptions = new ODataQueryOptions(queryOptions.Context, request);
        }

        return base.ApplyQuery(queryable, queryOptions);
    }
}

Simply:

?$filter=contains(tolower(siteName),tolower(%27michel%27))
                           ---^---            ---^---
                            Field           value to find

?$filter=contains(tolower(siteName),tolower(%27Michel%27))
?$filter=contains(tolower(siteName),tolower(%27MiCHel%27))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM