예제 #1
0
        protected override void VisitConvert(ODataConvertExpression node)
        {
            var convertExpression = node.Expression.Type.IsImplicityCastableTo(node.Type)
                ? node.Expression
                : ODataExpression.Call(ODataFunction.Cast, new[] { node.Expression, ODataExpression.Constant(node.ClrType) });

            this.Visit(convertExpression);
        }
            /// <summary>
            /// Attempts to translate the given <see cref="MemberExpression"/> as a special OData member, like <see cref="Nullable{T}.Value"/> or
            /// <see cref="string.Length"/>.
            /// </summary>
            private bool TryTranslateMemberAccessAsSpecialMember(MemberExpression memberAccess, out ODataExpression result)
            {
                // TODO FUTURE null handling?
                Func <IEnumerable <ODataExpression> > translateInstance = () => this._translator.TranslateInternal(memberAccess.Expression).Enumerate();

                if (memberAccess.Member.DeclaringType == typeof(string) && memberAccess.Member.Name == "Length")
                {
                    result = ODataExpression.Call(ODataFunction.Length, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Year")
                {
                    result = ODataExpression.Call(ODataFunction.Year, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Month")
                {
                    result = ODataExpression.Call(ODataFunction.Month, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Day")
                {
                    result = ODataExpression.Call(ODataFunction.Day, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Hour")
                {
                    result = ODataExpression.Call(ODataFunction.Hour, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Minute")
                {
                    result = ODataExpression.Call(ODataFunction.Minute, translateInstance());
                }
                else if (memberAccess.Member.DeclaringType == typeof(DateTime) && memberAccess.Member.Name == "Second")
                {
                    result = ODataExpression.Call(ODataFunction.Second, translateInstance());
                }
                // nullable properties
                else if (memberAccess.Member.Name == "HasValue" && memberAccess.Member.DeclaringType.IsGenericOfType(typeof(Nullable <>)))
                {
                    // for HasValue we just re-translate expr != null
                    result = this._translator.TranslateInternal(Expression.NotEqual(memberAccess.Expression, Expression.Constant(null, memberAccess.Expression.Type)));
                }
                else if (memberAccess.Member.Name == "Value" && memberAccess.Member.DeclaringType.IsGenericOfType(typeof(Nullable <>)))
                {
                    // .Value calls can just be ignored, since OData doesn't have the notion of nullable types
                    result = this._translator.TranslateInternal(memberAccess.Expression);
                }
                else
                {
                    result = null;
                    return(false);
                }

                return(true);
            }
예제 #3
0
        private ODataExpression ParseSimple()
        {
            if (this.TryEat(ODataTokenKind.LeftParen))
            {
                // parse group
                var group = this.ParseExpression();
                this.Eat(ODataTokenKind.RightParen);
                return(group);
            }

            ODataToken next;

            if (this.TryEat(ODataTokenKind.Identifier, out next))
            {
                if (this.TryEat(ODataTokenKind.LeftParen))
                {
                    // parse function
                    ODataFunction function;
                    if (!Enum.TryParse(next.Text, ignoreCase: true, result: out function))
                    {
                        throw new ODataParseException(next.Text + " is not a known ODataFunction!");
                    }
                    var arguments = this.ParseExpressionList(this.ParseExpression, ODataTokenKind.Comma);
                    this.Eat(ODataTokenKind.RightParen);

                    if (function == ODataFunction.IsOf || function == ODataFunction.Cast)
                    {
                        var typeLiteral   = this.ReParseAsType(arguments[arguments.Count - 1]);
                        var argumentsCopy = arguments.ToArray();
                        argumentsCopy[arguments.Count - 1] = ODataExpression.Constant(typeLiteral);
                        arguments = argumentsCopy;
                    }

                    return(ODataExpression.Call(function, arguments));
                }

                // parse member access
                var type = this._elementType;                 // root element type
                ODataMemberAccessExpression access = null;    // root member
                while (true)
                {
                    // get the property
                    var property = typeof(ODataObject).IsAssignableFrom(type)
                        ? ODataEntity.GetProperty(next.Text, typeof(ODataObject))
                        : type.GetProperty(next.Text, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                    if (property == null)
                    {
                        throw new ODataParseException("Property '" + next.Text + "' could not be found on type " + type.FullName);
                    }
                    access = ODataExpression.MemberAccess(access, property);

                    // if we don't see '/' followed by an identifier, we're done
                    // we can't just use TryEat() here, because we need to distinguish between Foo/Bar/* and Foo/Bar/Baz
                    if (this.Next().Kind != ODataTokenKind.Slash || this.Next(2).Kind != ODataTokenKind.Identifier)
                    {
                        break;
                    }

                    // otherwise, update next to the next id and then advance type down the property chain
                    this.Eat(ODataTokenKind.Slash);
                    next = this.Eat(ODataTokenKind.Identifier);
                    type = property.PropertyType;
                }
                return(access);
            }

            // literals
            if (this.TryEat(ODataTokenKind.NullLiteral, out next))
            {
                return(ODataExpression.Constant(null));
            }
            if (this.TryEat(ODataTokenKind.BinaryLiteral, out next))
            {
                throw new NotImplementedException("Binary Literal Parse");
            }
            if (this.TryEat(ODataTokenKind.BooleanLiteral, out next))
            {
                return(ODataExpression.Constant(bool.Parse(next.Text)));
            }
            // see comment on the enum
            //if (this.TryEat(ODataTokenKind.ByteLiteral, out next))
            //{
            //	return ODataExpression.Constant(Convert.ToByte(next.Text, fromBase: 16));
            //}
            if (this.TryEat(ODataTokenKind.DateTimeLiteral, out next))
            {
                Func <string, string> zeroIfEmpty = s => s.Length == 0 ? "0" : s;
                var dateTime = new DateTime(
                    year: int.Parse(next.Match.Groups["year"].Value),
                    month: int.Parse(next.Match.Groups["month"].Value),
                    day: int.Parse(next.Match.Groups["day"].Value),
                    hour: int.Parse(next.Match.Groups["hour"].Value),
                    minute: int.Parse(next.Match.Groups["minute"].Value),
                    second: int.Parse(zeroIfEmpty(next.Match.Groups["second"].Value)),
                    millisecond: 0
                    )
                               .AddSeconds(double.Parse(zeroIfEmpty(next.Match.Groups["fraction"].Value)));
                return(ODataExpression.Constant(dateTime));
            }
            if (this.TryEat(ODataTokenKind.DecimalLiteral, out next))
            {
                return(ODataExpression.Constant(decimal.Parse(next.Text.Substring(0, next.Text.Length - 1))));
            }
            if (this.TryEat(ODataTokenKind.DoubleLiteral, out next))
            {
                return(ODataExpression.Constant(double.Parse(next.Text)));
            }
            if (this.TryEat(ODataTokenKind.SingleLiteral, out next))
            {
                return(ODataExpression.Constant(float.Parse(next.Text.Substring(0, next.Text.Length - 1))));
            }
            if (this.TryEat(ODataTokenKind.GuidLiteral, out next))
            {
                return(ODataExpression.Constant(Guid.Parse(next.Match.Groups["digits"].Value)));
            }
            if (this.TryEat(ODataTokenKind.Int32Literal, out next))
            {
                // Note: this will fail hard if we have an out-of-range int value. However, this is consistent
                // with MSFT's implementation (see http://services.odata.org/v3/odata/odata.svc/Products?$format=json&$filter=Price%20ge%202147483648)
                return(ODataExpression.Constant(int.Parse(next.Text)));
            }
            if (this.TryEat(ODataTokenKind.Int64Literal, out next))
            {
                return(ODataExpression.Constant(long.Parse(next.Text.Substring(0, next.Text.Length - 1))));
            }
            if (this.TryEat(ODataTokenKind.StringLiteral, out next))
            {
                // unescaping, from http://stackoverflow.com/questions/3979367/how-to-escape-a-single-quote-to-be-used-in-an-odata-query
                return(ODataExpression.Constant(next.Match.Groups["chars"].Value.Replace("''", "'")));
            }

            throw new ODataParseException("Unexpected token " + this.Next());
        }
예제 #4
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());
            }
        }
예제 #5
0
        private ODataExpression TranslateInternal(Expression linq)
        {
            if (linq == null)
            {
                return(null);
            }

            // captured parameters
            object value;

            if (TryGetValueFast(linq, out value))
            {
                // special case the handling of queryable constants, since these can be the "root"
                // of the query expression tree. We check for isInsideQuery since we don't allow multiple
                // roots. An example of a multiply-rooted tree would be: q.Where(x => q.Any(xx => xx.A < x.A))
                if (!this._isInsideQuery && typeof(IQueryable).GetTypeInfo().IsAssignableFrom(linq.Type.GetTypeInfo()))
                {
                    this._rootQuery = (IQueryable)value;
                    return(ODataExpression.Query());
                }
                return(ODataExpression.Constant(value, linq.Type));
            }

            switch (linq.NodeType)
            {
            case ExpressionType.OrElse:
                return(this.TranslateBinary(linq, ODataBinaryOp.Or));

            case ExpressionType.AndAlso:
                return(this.TranslateBinary(linq, ODataBinaryOp.And));

            case ExpressionType.Add:
                if (linq.Type == typeof(string))
                {
                    // special case adding strings, since that's the same as Concat
                    var binary = (BinaryExpression)linq;
                    return(this.TranslateInternal(Expression.Call(Helpers.GetMethod(() => string.Concat(default(string), default(string))), binary.Left, binary.Right)));
                }
                return(this.TranslateBinary(linq, ODataBinaryOp.Add));

            case ExpressionType.Subtract:
                return(this.TranslateBinary(linq, ODataBinaryOp.Subtract));

            case ExpressionType.Multiply:
                return(this.TranslateBinary(linq, ODataBinaryOp.Multiply));

            case ExpressionType.Divide:
                return(this.TranslateBinary(linq, ODataBinaryOp.Divide));

            case ExpressionType.Modulo:
                return(this.TranslateBinary(linq, ODataBinaryOp.Modulo));

            case ExpressionType.Equal:
                return(this.TranslateBinary(linq, ODataBinaryOp.Equal));

            case ExpressionType.NotEqual:
                return(this.TranslateBinary(linq, ODataBinaryOp.NotEqual));

            case ExpressionType.LessThan:
                return(this.TranslateBinary(linq, ODataBinaryOp.LessThan));

            case ExpressionType.LessThanOrEqual:
                return(this.TranslateBinary(linq, ODataBinaryOp.LessThanOrEqual));

            case ExpressionType.GreaterThan:
                return(this.TranslateBinary(linq, ODataBinaryOp.GreaterThan));

            case ExpressionType.GreaterThanOrEqual:
                return(this.TranslateBinary(linq, ODataBinaryOp.GreaterThanOrEqual));

            case ExpressionType.Not:
                return(ODataExpression.UnaryOp(this.TranslateInternal(((UnaryExpression)linq).Operand), ODataUnaryOp.Not));

            case ExpressionType.Negate:
                // we translate -a.B as (-1 * a.B) for numeric types
                var negated = ((UnaryExpression)linq).Operand;
                var underlyingNegatedType = Nullable.GetUnderlyingType(negated.Type) ?? negated.Type;
                if (!underlyingNegatedType.IsNumeric())
                {
                    throw new ODataCompileException("Expression '" + linq + "' could not be translated to OData: only negation of numeric types is supported");
                }
                var multiplyNegate = Expression.Multiply(
                    Expression.Constant(Convert.ChangeType(-1, underlyingNegatedType), negated.Type),
                    negated
                    );
                return(this.TranslateInternal(multiplyNegate));

            case ExpressionType.MemberAccess:
                return(this._memberAndParameterTranslator.TranslateMemberAccess((MemberExpression)linq));

            case ExpressionType.Convert:
            case ExpressionType.ConvertChecked:
            case ExpressionType.TypeAs:
                return(ODataExpression.Convert(this.TranslateInternal(((UnaryExpression)linq).Operand), linq.Type));

            case ExpressionType.Parameter:
                return(this._memberAndParameterTranslator.TranslateParameter((ParameterExpression)linq));

            case ExpressionType.TypeIs:
                var typeBinary     = (TypeBinaryExpression)linq;
                var isArg          = this.TranslateInternal(typeBinary.Expression);
                var isTypeConstant = ODataExpression.Constant(typeBinary.TypeOperand);
                return(isArg != null
                                                ? ODataExpression.Call(ODataFunction.IsOf, new[] { isArg, isTypeConstant })
                                                : ODataExpression.Call(ODataFunction.IsOf, new[] { isTypeConstant }));

            case ExpressionType.Call:
                return(this.TranslateCall((MethodCallExpression)linq));

            case ExpressionType.Quote:
                var quoted = (LambdaExpression)((UnaryExpression)linq).Operand;
                if (!this._isInsideQuery)
                {
                    throw new ODataCompileException("Unexpected placement for lambda expression " + linq);
                }
                return(this.TranslateInternal(quoted.Body));

            default:
                throw new ODataCompileException("Expression '" + linq + "' of type " + linq.NodeType + " could not be translated to OData");
            }
        }
예제 #6
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");
        }