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); } }
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"); }
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()); } }
private ODataExpression TranslateBinary(Expression linq, ODataBinaryOp op) { var binary = (BinaryExpression)linq; return(ODataExpression.BinaryOp(this.TranslateInternal(binary.Left), op, this.TranslateInternal(binary.Right))); }