예제 #1
0
        private ODataExpression ParseBinaryExpressionHelper(Func <ODataExpression> parse, IReadOnlyDictionary <ODataTokenKind, ODataBinaryOp> binaryOpMapping)
        {
            var result = parse();

OUTER:
            while (true)
            {
                foreach (var kvp in binaryOpMapping)
                {
                    if (this.TryEat(kvp.Key))
                    {
                        result = ODataExpression.BinaryOp(result, kvp.Value, parse());
                        goto OUTER;
                    }
                }
                return(result);
            }
        }
예제 #2
0
        private ODataExpression TranslateCall(MethodCallExpression call)
        {
            // query operators
            if (call.Method.DeclaringType == typeof(Queryable))
            {
                if (this._isInsideQuery)
                {
                    throw new ODataCompileException("OData does not support nested query structures!");
                }

                // normalize overloads of query operators
                bool changedAllToAny;
                var  normalized = QueryOperatorCanonicalizer.Canonicalize(call, changedAllToAny: out changedAllToAny);
                if (normalized != call)
                {
                    // TODO FUTURE better exception message here since expression being translated won't match?
                    var result = this.TranslateInternal(normalized);
                    if (changedAllToAny)
                    {
                        var innerResultTranslator = this._resultTranslator;
                        Throw <InvalidOperationException> .If(innerResultTranslator == null, "Sanity check: expected non-null result translator");

                        this._resultTranslator = (values, count) => !((bool)innerResultTranslator(values, count));
                    }
                    return(result);
                }

                // handle "normal" query methods

                // translate the source query. If the source is a constant, that's the "root" of the query tree
                // e. g. that might be the OData equivalent of a DbSet or ObjectQuery constant
                var source = (ODataQueryExpression)this.TranslateInternal(call.Arguments[0]);
                switch (call.Method.Name)
                {
                case "Where":
                    // not the index version
                    if (call.Arguments[1].Type.IsGenericOfType(typeof(Func <, ,>)))
                    {
                        goto default;
                    }

                    var predicate = this.TranslateInsideQuery(call.Arguments[1]);
                    if (source.OrderBy.Count > 0 || source.Top.HasValue || source.Skip > 0)
                    {
                        throw new ODataCompileException("Cannot apply a filter after applying an OrderBy, Take, or Skip operation");
                    }
                    return(source.Update(filter: source.Filter != null ? ODataExpression.BinaryOp(source.Filter, ODataBinaryOp.And, predicate) : predicate));

                case "OrderBy":
                case "OrderByDescending":
                case "ThenBy":
                case "ThenByDescending":
                    if (call.Arguments.Count != 2)
                    {
                        goto default;
                    }

                    var sortKeyExpression = this.TranslateInsideQuery(call.Arguments[1]);
                    var sortKey           = ODataExpression.SortKey(sortKeyExpression, descending: call.Method.Name.EndsWith("Descending"));
                    if (source.Top.HasValue || source.Skip > 0)
                    {
                        throw new ODataCompileException("Cannot apply a sort after applying Take or Skip operations");
                    }
                    return(source.Update(
                               orderBy: call.Method.Name.StartsWith("Then")
                                                                ? source.OrderBy.Concat(sortKey.Enumerate())
                                                                : sortKey.Enumerate()
                               ));

                case "Skip":
                    object skip;
                    Throw <InvalidOperationException> .If(!TryGetValueFast(call.Arguments[1], out skip), "Could not get value");

                    if (source.Top.HasValue)
                    {
                        throw new ODataCompileException("Cannot apply a skip after applying a Take operation");
                    }
                    return(source.Update(skip: source.Skip + (int)skip));                            // not right

                case "Take":
                    object take;
                    Throw <InvalidOperationException> .If(!TryGetValueFast(call.Arguments[1], out take), "Could not get value");

                    return(Take(source, (int)take));

                case "Select":
                    /*
                     * MA: select is tricky in OData, since it doesn't just conform to the OData $select system query option.
                     * Instead, you could have an intermediate select in your query, and it would be nice to be able to support that.
                     * In that case, OData still forces us to select original columns, but we can store away the projection expressions
                     * to re-apply later. We also need to make sure to store how to map each projected property, so that we can inline these
                     * projections.
                     *
                     * For example, imagine you have the query q.Select(x => new { a = x.B + 2 }).Where(t => t.a > 5).
                     * In OData, this would translate to "$filter=B + 2 > 5&$select=B". We can then do the final projection to the anonymous
                     * type in memory on the client side
                     */

                    // we don't support select with index
                    if (!call.Arguments[1].Type.GetGenericArguments(typeof(Expression <>)).Single().IsGenericOfType(typeof(Func <,>)))
                    {
                        goto default;
                    }

                    // unquote and extract the lambda
                    var projection = ((LambdaExpression)((UnaryExpression)call.Arguments[1]).Operand);

                    // register the projection
                    this._isInsideQuery = true;
                    this._memberAndParameterTranslator.RegisterProjection(projection);
                    this._isInsideQuery = false;

                    // return the source, since the projection doesn't actually affect the returned expression
                    // until the very end when we can use it to determine which columns to $select
                    return(source);

                case "Any":
                    this._resultTranslator = MakeTranslator(call.Arguments[0], e => e.Any());
                    return(Take(source, 1));

                case "Count":
                    this._resultTranslator = (values, count) => ComputeCount(count: count.Value, skip: source.Skip, top: source.Top);
                    // top 0 is so that we don't have any wasted payload of data that we won't look at
                    return(source.Update(inlineCount: ODataInlineCountOption.AllPages, top: 0));

                case "LongCount":
                    this._resultTranslator = (values, count) => (long)ComputeCount(count: count.Value, skip: source.Skip, top: source.Top);
                    // top 0 is so that we don't have any wasted payload of data that we won't look at
                    return(source.Update(inlineCount: ODataInlineCountOption.AllPages, top: 0));

                case "First":
                    this._resultTranslator = MakeTranslator(call.Arguments[0], e => e.First());
                    return(Take(source, 1));

                case "FirstOrDefault":
                    this._resultTranslator = MakeTranslator(call.Arguments[0], e => e.FirstOrDefault());
                    return(Take(source, 1));

                case "Single":
                    this._resultTranslator = MakeTranslator(call.Arguments[0], e => e.Single());;
                    return(Take(source, 2));

                case "SingleOrDefault":
                    this._resultTranslator = MakeTranslator(call.Arguments[0], e => e.SingleOrDefault());;
                    return(Take(source, 2));

                default:
                    throw new ODataCompileException("Query operator " + call.Method + " is not supported in OData");
                }
            }

            // other OData methods

            // Enumerable/Collection contains (e. g. "IN"). This gets handled before the odata functions because
            // the thisExpression can't be translated normally. Since Contains() is declared on a bunch of different collection
            // types, we basically check that (1) there are either 2 arguments (static method) or an instance + 1 argument
            // (2) that the container argument/instance is an in-memory "constant", (3) that the collection object has an IEnumerable<T>
            // element type, and (4) that the test argument is of that element type
            // Finally, we translate the IN clause to a set of ORs
            object enumerable;
            Type   elementType;

            if (call.Method.Name == "Contains" &&
                call.Arguments.Count + Convert.ToInt32(!call.Method.IsStatic) == 2 &&
                TryGetValueFast(call.Object ?? call.Arguments[0], out enumerable) &&
                enumerable != null &&
                (elementType = enumerable.GetType().GetGenericArguments(typeof(IEnumerable <>)).SingleOrDefault()) != null &&
                call.Arguments.Last().Type == elementType)
            {
                var testExpression           = this.TranslateInternal(call.Arguments.Last());
                var equalsElementExpressions = ((IEnumerable)enumerable).Cast <object>()
                                               .Select(o => ODataExpression.Constant(o, elementType))
                                               .Select(c => ODataExpression.BinaryOp(testExpression, ODataBinaryOp.Equal, c))
                                               .ToArray();
                var equivalentOrExpression = equalsElementExpressions.Length == 0
                                        ? ODataExpression.Constant(true).As <ODataExpression>()
                                        : equalsElementExpressions.Aggregate((e1, e2) => ODataExpression.BinaryOp(e1, ODataBinaryOp.Or, e2));
                return(equivalentOrExpression);
            }

            // ODataFunctions
            var thisExpression = this.TranslateInternal(call.Object);
            var translatedArgs = call.Arguments.Select(this.TranslateInternal);

            // string functions
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "Substring")
            {
                return(ODataExpression.Call(ODataFunction.Substring, thisExpression.Enumerate().Concat(translatedArgs)));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "Replace" && call.Arguments[0].Type == typeof(string))
            {
                // for now, we don't support the char replace overload
                return(ODataExpression.Call(ODataFunction.Replace, thisExpression.Enumerate().Concat(translatedArgs)));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "Concat" && call.Arguments.All(a => a.Type == typeof(string)))
            {
                // we support only string concats, but with any fixed number of parameters
                return(translatedArgs.Aggregate((s1, s2) => ODataExpression.Call(ODataFunction.Concat, new[] { s1, s2 })));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "StartsWith" && call.Arguments.Count == 1)
            {
                return(ODataExpression.Call(ODataFunction.StartsWith, new[] { thisExpression, translatedArgs.Single() }));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "EndsWith" && call.Arguments.Count == 1)
            {
                return(ODataExpression.Call(ODataFunction.EndsWith, new[] { thisExpression, translatedArgs.Single() }));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "IndexOf" && call.Arguments.Count == 1 && call.Arguments[0].Type == typeof(string))
            {
                return(ODataExpression.Call(ODataFunction.IndexOf, new[] { thisExpression, translatedArgs.Single() }));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "Contains" && call.Arguments.Count == 1 && call.Arguments[0].Type == typeof(string))
            {
                // note: we reverse the args here because A.SubstringOf(B) is equivalent to B.Contains(A)
                return(ODataExpression.Call(ODataFunction.SubstringOf, new[] { translatedArgs.Single(), thisExpression }));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "ToLower" && call.Arguments.Count == 0)
            {
                return(ODataExpression.Call(ODataFunction.ToLower, thisExpression.Enumerate()));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "ToUpper" && call.Arguments.Count == 0)
            {
                return(ODataExpression.Call(ODataFunction.ToUpper, thisExpression.Enumerate()));
            }
            if (call.Method.DeclaringType == typeof(string) && call.Method.Name == "Trim" && call.Arguments.Count == 0)
            {
                return(ODataExpression.Call(ODataFunction.Trim, thisExpression.Enumerate()));
            }

            // math functions
            if (call.Method.DeclaringType == typeof(Math) && call.Method.Name == "Ceiling")
            {
                return(ODataExpression.Call(ODataFunction.Ceiling, translatedArgs));
            }
            if (call.Method.DeclaringType == typeof(Math) && call.Method.Name == "Floor")
            {
                return(ODataExpression.Call(ODataFunction.Floor, translatedArgs));
            }
            if (call.Method.DeclaringType == typeof(Math) && call.Method.Name == "Round" && call.Arguments.Count == 1)
            {
                return(ODataExpression.Call(ODataFunction.Round, translatedArgs));
            }

            throw new ODataCompileException("Method " + call.Method + " could not be translated to OData");
        }
예제 #3
0
        protected override void VisitCall(ODataCallExpression node)
        {
            // reference for different function syntax http://users.atw.hu/sqlnut/sqlnut2-chp-4-sect-4.html
            switch (node.Function)
            {
            case ODataFunction.Cast:
                this.Write("CAST(").Write(node.Arguments[0]);
                var toType = ((Type)((ODataConstantExpression)node.Arguments[1]).Value).ToODataExpressionType();
                this.Write(" AS ").Write(this.syntaxProvider.GetSqlTypeName(toType)).Write(")");
                break;

            case ODataFunction.Ceiling:
                this.VisitCallHelper(this.syntaxProvider.UseAbbreviatedCeilingFunction ? "CEIL" : "CEILING", node);
                break;

            case ODataFunction.Concat:
                this.Write("(").Write(node.Arguments[0])
                .Write(" + ")
                .Write(node.Arguments[1]).Write(")");
                break;

            case ODataFunction.Second:
            case ODataFunction.Minute:
            case ODataFunction.Hour:
            case ODataFunction.Day:
            case ODataFunction.Month:
            case ODataFunction.Year:
                this.syntaxProvider.RenderDatePartFunctionCall(node.Function, s => this.Write(s), () => this.Write(node.Arguments[0]));
                break;

            case ODataFunction.EndsWith:
                // endswith => (INDEXOF(needle, haystack) = LEN(haystack) - LEN(needle)) OR LEN(needle) = 0
                var needleLengthExpression = ODataExpression.Call(ODataFunction.Length, new[] { node.Arguments[1] });
                var endsWithExpression     = ODataExpression.BinaryOp(
                    ODataExpression.BinaryOp(
                        ODataExpression.Call(ODataFunction.IndexOf, node.Arguments),
                        ODataBinaryOp.Equal,
                        ODataExpression.BinaryOp(
                            ODataExpression.Call(ODataFunction.Length, new[] { node.Arguments[0] }),
                            ODataBinaryOp.Subtract,
                            needleLengthExpression
                            )
                        ),
                    ODataBinaryOp.Or,
                    ODataExpression.BinaryOp(
                        needleLengthExpression,
                        ODataBinaryOp.Equal,
                        ODataExpression.Constant(0)
                        )
                    );
                this.Visit(endsWithExpression);
                break;

            case ODataFunction.StartsWith:
                // startswith => INDEXOF(needle, haystack) = 0
                var startsWithExpression = ODataExpression.BinaryOp(
                    ODataExpression.Call(ODataFunction.IndexOf, node.Arguments),
                    ODataBinaryOp.Equal,
                    ODataExpression.Constant(0)
                    );
                this.Visit(startsWithExpression);
                break;

            case ODataFunction.SubstringOf:
                // substringof => INDEXOF(needle, haystack) >= 0
                var substringOfExpression = ODataExpression.BinaryOp(
                    ODataExpression.Call(ODataFunction.IndexOf, node.Arguments.Reverse()),
                    ODataBinaryOp.GreaterThanOrEqual,
                    ODataExpression.Constant(0)
                    );
                this.Visit(substringOfExpression);
                break;

            case ODataFunction.IndexOf:
                this.syntaxProvider.RenderIndexOfFunctionCall(s => this.Write(s), renderNeedleArgument: () => this.Write(node.Arguments[1]), renderHaystackArgument: () => this.Write(node.Arguments[0]));
                break;

            case ODataFunction.Floor:
                this.VisitCallHelper("FLOOR", node);
                break;

            case ODataFunction.Length:
                this.VisitCallHelper(this.syntaxProvider.StringLengthFunctionName, node);
                break;

            case ODataFunction.Replace:
                this.VisitCallHelper("REPLACE", node);
                break;

            case ODataFunction.Round:
                this.syntaxProvider.RenderRoundFunctionCall(s => this.Write(s), () => this.Write(node.Arguments[0]));
                break;

            case ODataFunction.Substring:
                this.syntaxProvider.RenderSubstringFunctionCall(s => this.Write(s), () => this.Write(node.Arguments[0]), () => this.Write(node.Arguments[1]), node.Arguments.Count > 2 ? new Action(() => this.Write(node.Arguments[2])) : null);
                break;

            case ODataFunction.ToLower:
                this.VisitCallHelper("LOWER", node);
                break;

            case ODataFunction.ToUpper:
                this.VisitCallHelper("UPPER", node);
                break;

            case ODataFunction.Trim:
                if (this.syntaxProvider.HasTwoSidedTrim)
                {
                    this.VisitCallHelper("TRIM", node);
                }
                else
                {
                    // call both LTRIM and RTRIM
                    this.VisitCallHelper("LTRIM(RTRIM", node);
                    this.Write(")");
                }
                break;

            case ODataFunction.IsOf:
                throw new NotSupportedException(node.Function.ToString());
            }
        }
예제 #4
0
        private ODataExpression TranslateBinary(Expression linq, ODataBinaryOp op)
        {
            var binary = (BinaryExpression)linq;

            return(ODataExpression.BinaryOp(this.TranslateInternal(binary.Left), op, this.TranslateInternal(binary.Right)));
        }