Ejemplo n.º 1
0
        static void Main(string[] args)
        {
            var xxx = ResultTranslator <AllResultsWrapper>
                      .GetTranslator()
                      .Translate(r => r.FirstResult)
                      .Translate(r => r.SecondResult)
                      .Run();



            Console.ReadLine();
        }
Ejemplo n.º 2
0
 void IInitializable.Initialize()
 {
     ResultEntitySubject
     .OnNext(
         ResultEntityFactory
         .Create(
             ScoreEntity.Current.Value,
             PlayerPrefs.GetString(Constant.PlayerPrefsKey.LastPlayerName, string.Empty),
             DateTime.Now
             )
         );
     ResultEntitySubject.OnCompleted();
     GameResultHandler.RenderResult(ResultTranslator.Translate(ResultEntitySubject.Value));
     GameResultHandler.UpdatePlayerNameAsObservable().Subscribe(ResultEntitySubject.Value.UpdatePlayerName);
 }
Ejemplo n.º 3
0
        public ODataExpression Translate(Expression linq, out IQueryable rootQuery, out ResultTranslator resultTranslator)
        {
            this._isInsideQuery                = false;
            this._rootQuery                    = null;
            this._resultTranslator             = null;
            this._memberAndParameterTranslator = new MemberAndParameterTranslator(this);

            // normalize away ODataRow constructs
            var normalized = ODataEntity.Normalize(linq);

            var translated = this.TranslateInternal(normalized);

            if (translated.Kind == ODataExpressionKind.Query)
            {
                var referencedPaths = this._memberAndParameterTranslator.GetReferencedMemberPathsInFinalProjection();
                if (referencedPaths != null)
                {
                    var selectColumns = referencedPaths.Select(p => p.Aggregate(default(ODataMemberAccessExpression), (e, m) => ODataExpression.MemberAccess(e, (PropertyInfo)m)))
                                        .Select(ma => ODataExpression.SelectColumn(ma, allColumns: ma.Type == ODataExpressionType.Complex));
                    translated = ((ODataQueryExpression)translated).Update(select: selectColumns);
                }
            }

            rootQuery = this._rootQuery;

            var projection      = this._memberAndParameterTranslator.GetFinalProjection();
            var finalTranslator = this._resultTranslator ?? ((values, count) => values);

            if (projection != null)
            {
                var selectMethod = Helpers.GetMethod((IEnumerable <object> e) => e.Select(o => o))
                                   .GetGenericMethodDefinition()
                                   .MakeGenericMethod(projection.Type.GetGenericArguments(typeof(Func <,>)));

                // restores any ODataRow constructs that were normalized away, since we need to be able to compile and run the projection
                // (i. e. fake ODataRow property accesses don't run when compiled)
                var denormalizedProjection = (LambdaExpression)ODataEntity.Denormalize(projection);

                Func <object, object> queryTranslator = enumerable => selectMethod.Invoke(null, new[] { enumerable, denormalizedProjection.Compile() });
                resultTranslator = (values, count) => finalTranslator((IEnumerable)queryTranslator(values), count);
            }
            else
            {
                resultTranslator = finalTranslator;
            }
            return(translated);
        }
        public ODataExpression Translate(Expression linq, out IQueryable rootQuery, out ResultTranslator resultTranslator)
        {
            this._isInsideQuery = false;
            this._rootQuery = null;
            this._resultTranslator = null;
            this._memberAndParameterTranslator = new MemberAndParameterTranslator(this);

            // normalize away ODataRow constructs
            var normalized = ODataEntity.Normalize(linq);

            var translated = this.TranslateInternal(normalized);

            if (translated.Kind == ODataExpressionKind.Query)
            {
                var referencedPaths = this._memberAndParameterTranslator.GetReferencedMemberPathsInFinalProjection();
                if (referencedPaths != null)
                {
                    var selectColumns = referencedPaths.Select(p => p.Aggregate(default(ODataMemberAccessExpression), (e, m) => ODataExpression.MemberAccess(e, (PropertyInfo)m)))
                        .Select(ma => ODataExpression.SelectColumn(ma, allColumns: ma.Type == ODataExpressionType.Complex));
                    translated = ((ODataQueryExpression)translated).Update(select: selectColumns);
                }
            }

            rootQuery = this._rootQuery;

            var projection = this._memberAndParameterTranslator.GetFinalProjection();
            var finalTranslator = this._resultTranslator ?? ((values, count) => values);
            if (projection != null)
            {
                var selectMethod = Helpers.GetMethod((IEnumerable<object> e) => e.Select(o => o))
                    .GetGenericMethodDefinition()
                    .MakeGenericMethod(projection.Type.GetGenericArguments(typeof(Func<,>)));

                // restores any ODataRow constructs that were normalized away, since we need to be able to compile and run the projection
                // (i. e. fake ODataRow property accesses don't run when compiled)
                var denormalizedProjection = (LambdaExpression)ODataEntity.Denormalize(projection);

                Func<object, object> queryTranslator = enumerable => selectMethod.Invoke(null, new[] { enumerable, denormalizedProjection.Compile() });
                resultTranslator = (values, count) => finalTranslator((IEnumerable)queryTranslator(values), count);
            }
            else
            {
                resultTranslator = finalTranslator;
            }
            return translated;
        }
        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) => Math.Min(count.Value - source.Skip, source.Top ?? int.MaxValue);
                        // 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)Math.Min(count.Value - source.Skip, source.Top ?? int.MaxValue);
                        // 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");
        }
Ejemplo n.º 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");
        }