From 1180c17caca8ba615231ab82ed107982d5d7e100 Mon Sep 17 00:00:00 2001 From: cdroulers Date: Thu, 21 May 2015 19:16:25 -0400 Subject: [PATCH 1/5] Add support for $filter=substring(Name,1,2) --- Settings.StyleCop | 15 ++ .../Internal/FixStringMethodsVisitor.cs | 179 +++++++------- .../Internal/GivenNHibernateQuery.cs | 220 +++++++++--------- webapi.nhibernate-odata.sln.GhostDoc.xml | 6 + 4 files changed, 226 insertions(+), 194 deletions(-) create mode 100644 webapi.nhibernate-odata.sln.GhostDoc.xml diff --git a/Settings.StyleCop b/Settings.StyleCop index 2f20031..5af3478 100644 --- a/Settings.StyleCop +++ b/Settings.StyleCop @@ -247,6 +247,11 @@ False + + + False + + @@ -371,6 +376,16 @@ False + + + False + + + + + False + + diff --git a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs index cf5c425..b6df943 100644 --- a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs +++ b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs @@ -5,97 +5,112 @@ namespace Pathoschild.WebApi.NhibernateOdata.Internal { - /// Intercepts queries before they're parsed by NHibernate to rewrite unsupported lambdas for , and . - /// - /// The expression tree generated by the ODataQueryOptions.ApplyTo method looks like the following sample. - /// - /// .Lambda #Lambda1<System.Func`2[Pathoschild.WebApi.NhibernateOdata.Tests.Models.Parent,System.Boolean]>(Pathoschild.WebApi.NhibernateOdata.Tests.Models.Parent $$it) - /// { - /// (.If ( - /// $$it.Name == null | .Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty == - /// null - /// ) { - /// null - /// } .Else { - /// (System.Nullable`1[System.Boolean]).Call ($$it.Name).Contains(.Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty) - /// } == (System.Nullable`1[System.Boolean]).Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.Boolean]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.Boolean]).TypedProperty) - /// == .Constant<System.Nullable`1[System.Boolean]>(True) - /// } - /// - /// - public class FixStringMethodsVisitor : ExpressionVisitor - { - /********* - ** Properties - *********/ - /// Whether the visitor is visiting a nested node. - /// This is used to recognize the top-level node for logging. - private bool IsRecursing; + /// Intercepts queries before they're parsed by NHibernate to rewrite unsupported lambdas for , and . + /// + /// The expression tree generated by the ODataQueryOptions.ApplyTo method looks like the following sample. + /// + /// .Lambda #Lambda1<System.Func`2[Pathoschild.WebApi.NhibernateOdata.Tests.Models.Parent,System.Boolean]>(Pathoschild.WebApi.NhibernateOdata.Tests.Models.Parent $$it) + /// { + /// (.If ( + /// $$it.Name == null | .Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty == + /// null + /// ) { + /// null + /// } .Else { + /// (System.Nullable`1[System.Boolean]).Call ($$it.Name).Contains(.Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty) + /// } == (System.Nullable`1[System.Boolean]).Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.Boolean]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.Boolean]).TypedProperty) + /// == .Constant<System.Nullable`1[System.Boolean]>(True) + /// } + /// + /// + public class FixStringMethodsVisitor : ExpressionVisitor + { + /********* + ** Properties + *********/ + /// Whether the visitor is visiting a nested node. + /// This is used to recognize the top-level node for logging. + private bool IsRecursing; - /// A list of methods supported by this visitor. - private readonly List StringMethods = new List(); + /// A list of methods supported by this visitor. + private readonly List StringMethods = new List(); - /********* - ** Public methods - *********/ - /// Constructs an instance. - public FixStringMethodsVisitor() - { - this.StringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Contains" || x.Name == "StartsWith" || x.Name == "EndsWith")); - } + /********* + ** Public methods + *********/ + /// Constructs an instance. + public FixStringMethodsVisitor() + { + this.StringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Contains" || x.Name == "StartsWith" || x.Name == "EndsWith")); + } - /// Dispatches the expression to one of the more specialized visit methods in this class. - /// The expression to visit. - /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. - public override Expression Visit(Expression node) - { - // top node - if (!this.IsRecursing) - { - this.IsRecursing = true; - return base.Visit(node); - } + /// Dispatches the expression to one of the more specialized visit methods in this class. + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + public override Expression Visit(Expression node) + { + // top node + if (!this.IsRecursing) + { + this.IsRecursing = true; + return base.Visit(node); + } - var conditionalExpression = node as ConditionalExpression; - if (conditionalExpression != null) - return this.HandleConditionalExpression(node, conditionalExpression); + var conditionalExpression = node as ConditionalExpression; + if (conditionalExpression != null) + return this.HandleConditionalExpression(node, conditionalExpression); - return base.Visit(node); - } + return base.Visit(node); + } - /********* - ** Protected methods - *********/ - /// Handles the conditional expression (equivalent to .If {} .Else {} in the sample expression tree in the remarks). - /// The original expression. - /// The conditional expression. - /// A reduced if/else statement if it contains any of the matched methods. Otherwise, the original expression. - private Expression HandleConditionalExpression(Expression original, ConditionalExpression ifElse) - { - var elseExpression = ifElse.IfFalse as UnaryExpression; - if (elseExpression != null) - { - var methodCallExpression = elseExpression.Operand as MethodCallExpression; - if (methodCallExpression != null) - { - if (this.StringMethods.Contains(methodCallExpression.Method)) - { - var methodCallReplacement = Expression.Call( - methodCallExpression.Object, - methodCallExpression.Method, - methodCallExpression.Arguments); + /********* + ** Protected methods + *********/ + /// Handles the conditional expression (equivalent to .If {} .Else {} in the sample expression tree in the remarks). + /// The original expression. + /// The conditional expression. + /// A reduced if/else statement if it contains any of the matched methods. Otherwise, the original expression. + private Expression HandleConditionalExpression(Expression original, ConditionalExpression ifElse) + { + var elseExpression = ifElse.IfFalse as UnaryExpression; + if (elseExpression != null) + { + var methodCallExpression = elseExpression.Operand as MethodCallExpression; + if (methodCallExpression != null) + { + if (this.StringMethods.Contains(methodCallExpression.Method)) + { + var methodCallReplacement = Expression.Call( + methodCallExpression.Object, + methodCallExpression.Method, + methodCallExpression.Arguments); - // Convert the result to a nullable boolean so the Expression.Equal works. - var result = Expression.Convert(methodCallReplacement, typeof(bool?)); + // Convert the result to a nullable boolean so the Expression.Equal works. + var result = Expression.Convert(methodCallReplacement, typeof(bool?)); - return result; - } - } - } + return result; + } + } + } - return original; - } - } + var firstLevelMethodCallExpression = ifElse.IfFalse as MethodCallExpression; + if (firstLevelMethodCallExpression != null) + { + // Using the method name and declaring type as strings because I don't want to add a dependency to the project for a simple check like that. + if (firstLevelMethodCallExpression.Method.Name == "SubstringStartAndLength" && + firstLevelMethodCallExpression.Method.DeclaringType != null && + firstLevelMethodCallExpression.Method.DeclaringType.FullName == "System.Web.Http.OData.Query.Expressions.ClrSafeFunctions") + { + return Expression.Call( + firstLevelMethodCallExpression.Arguments[0], + typeof(string).GetMethod("Substring", new[] { typeof(int), typeof(int) }), + firstLevelMethodCallExpression.Arguments.Skip(1).ToArray()); + } + } + + return original; + } + } } \ No newline at end of file diff --git a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs index 9b009c4..78fec26 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -11,121 +11,117 @@ namespace Pathoschild.WebApi.NhibernateOdata.Tests.Internal { - // ReSharper disable InconsistentNaming - [TestFixture] - [Category("Integration")] - public class GivenNHibernateQuery - { - private static readonly ISessionFactory SessionFactory = NHibernateHelper.SessionFactory; - private ISession _session; - - [SetUp] - public void SetUp() - { - this._session = SessionFactory.OpenSession(); - } - - [TearDown] - public void TearDown() - { - this._session.Dispose(); - } - - [TestCase(true)] - [TestCase(false)] - public void When_querying_nullable_Then_queries_database(bool withVisitor) - { - var visitor = new FixNullableBooleanVisitor(); - var odataQuery = Helpers.Build("$filter=Parent/Id eq 61 and Id eq 11"); - var children = this._session.Query(); - - var results = odataQuery.ApplyTo(children).Cast(); - if (withVisitor) - { - results = results.InterceptWith(visitor); - } - - var list = results.ToList(); - Assert.That(list, Has.Count.EqualTo(1)); - } - - [Test] - [Ignore("Does not work yet, but it definitely should!")] - public void When_expanding_children_Then_works() - { - Console.WriteLine("What it should look like:"); - var r = this._session.Query().Select(x => new { x.Id, x.Children }).ToList(); - Console.WriteLine("{0} results", r.Count); - - var odataQuery = Helpers.Build("$select=Id,Children&$expand=Children"); - var parents = this._session.Query(); - - var results = odataQuery.ApplyTo(parents).Cast(); - - var json = JsonConvert.SerializeObject(results); - - Console.WriteLine(json); - } - - [Test] - public void When_projecting_one_column_Then_only_queries_one_column() - { - Console.WriteLine("What it should look like:"); - var query = this._session.Query().Select(x => new { x.Name }).ToList(); - Console.WriteLine("{0} results", query.Count); - - var odataQuery = Helpers.Build("$select=Name"); - var parents = this._session.Query(); - - var results = odataQuery.ApplyTo(parents).Cast(); - - var json = JsonConvert.SerializeObject(results); - - Console.WriteLine(json); - - // Would probably need to add a custom appender to log4net to query the query and make sure it's SELECT Name FROM whatever. - Assert.Inconclusive("Gotta check the output of NHibernate"); - } + // ReSharper disable InconsistentNaming + [TestFixture] + [Category("Integration")] + public class GivenNHibernateQuery + { + private static readonly ISessionFactory SessionFactory = NHibernateHelper.SessionFactory; + private ISession _session; + + [SetUp] + public void SetUp() + { + this._session = SessionFactory.OpenSession(); + } + + [TearDown] + public void TearDown() + { + this._session.Dispose(); + } + + [TestCase(true)] + [TestCase(false)] + public void When_querying_nullable_Then_queries_database(bool withVisitor) + { + var visitor = new FixNullableBooleanVisitor(); + var odataQuery = Helpers.Build("$filter=Parent/Id eq 61 and Id eq 11"); + var children = this._session.Query(); + + var results = odataQuery.ApplyTo(children).Cast(); + if (withVisitor) + { + results = results.InterceptWith(visitor); + } + + var list = results.ToList(); + Assert.That(list, Has.Count.EqualTo(1)); + } + + [Test] + [Ignore("Does not work yet, but it definitely should!")] + public void When_expanding_children_Then_works() + { + Console.WriteLine("What it should look like:"); + var r = this._session.Query().Select(x => new { x.Id, x.Children }).ToList(); + Console.WriteLine("{0} results", r.Count); + + var odataQuery = Helpers.Build("$select=Id,Children&$expand=Children"); + var parents = this._session.Query(); + + var results = odataQuery.ApplyTo(parents).Cast(); + + var json = JsonConvert.SerializeObject(results); + + Console.WriteLine(json); + } + + [Test] + public void When_projecting_one_column_Then_only_queries_one_column() + { + Console.WriteLine("What it should look like:"); + var query = this._session.Query().Select(x => new { x.Name }).ToList(); + Console.WriteLine("{0} results", query.Count); + + var odataQuery = Helpers.Build("$select=Name"); + var parents = this._session.Query(); + + var results = odataQuery.ApplyTo(parents).Cast(); + + var json = JsonConvert.SerializeObject(results); + + Console.WriteLine(json); + + // Would probably need to add a custom appender to log4net to query the query and make sure it's SELECT Name FROM whatever. + Assert.Inconclusive("Gotta check the output of NHibernate"); + } [TestCase("$filter=substringof('parent', Name) eq true", 2)] [TestCase("$filter=substringof('parent', Name) eq true and substringof('61', Name) eq false", 1)] - [TestCase("$filter=substringof('parent', Name)", 2)] - [TestCase("$filter=startswith(Name, 'parent') eq true", 2)] - [TestCase("$filter=endswith(Name, 'parent 61') eq true", 1)] - [TestCase("$filter=substringof('parent', Name) eq false", 0)] + [TestCase("$filter=substringof('parent', Name)", 2)] + [TestCase("$filter=startswith(Name, 'parent') eq true", 2)] + [TestCase("$filter=endswith(Name, 'parent 61') eq true", 1)] + [TestCase("$filter=substringof('parent', Name) eq false", 0)] [TestCase("$filter=not substringof('parent', Name)", 0)] [TestCase("$filter=not substringof('wot', Name) and startswith(Name, 'parent 61')", 1)] - [TestCase("$filter=startswith(Name, 'parent') eq false", 0)] - [TestCase("$filter=endswith(Name, 'parent 61') eq false", 1)] - //[TestCase("$filter=substring(Name, 1, 2) eq 'ar'", 2)] - public void When_filtering_one_column_with_methods_Then_uses_where_like(string filter, int resultCount) - { - var visitor = new FixStringMethodsVisitor(); - Console.WriteLine("What it should look like:"); - var r = this._session.Query().Where(x => x.Name.Contains("parent")); - //r = r.InterceptWith(visitor); - Console.WriteLine("{0} results", r.ToList().Count); - - var odataQuery = Helpers.Build(filter); - var parents = this._session.Query(); - parents = parents.InterceptWith(visitor); - - var results = odataQuery.ApplyTo(parents).Cast().ToList(); - Assert.That(results, Has.Count.EqualTo(resultCount)); - } - - [Test] - public void When_filtering_one_column_with_eq_Then_uses_where() - { - Console.WriteLine("What it should look like:"); - var r = this._session.Query().Where(x => x.Name == "parent"); - Console.WriteLine("{0} results", r.ToList().Count); - - var odataQuery = Helpers.Build("$filter=Name eq 'parent 61'"); - var parents = this._session.Query(); - - var results = odataQuery.ApplyTo(parents).Cast().ToList(); - Assert.That(results, Has.Count.EqualTo(1)); - } - } + [TestCase("$filter=startswith(Name, 'parent') eq false", 0)] + [TestCase("$filter=endswith(Name, 'parent 61') eq false", 1)] + [TestCase("$filter=substring(Name, 1, 2) eq 'ar'", 2)] + [TestCase("$filter=substring(Name, 1, 2) eq 'ar' and startswith(Name, 'par')", 2)] + public void When_filtering_one_column_with_methods_Then_uses_where_like(string filter, int resultCount) + { + var visitor = new FixStringMethodsVisitor(); + var odataQuery = Helpers.Build(filter); + var parents = this._session.Query(); + parents = parents.InterceptWith(visitor); + + var results = odataQuery.ApplyTo(parents).Cast().ToList(); + Assert.That(results, Has.Count.EqualTo(resultCount)); + } + + [Test] + public void When_filtering_one_column_with_eq_Then_uses_where() + { + Console.WriteLine("What it should look like:"); + var r = this._session.Query().Where(x => x.Name == "parent"); + Console.WriteLine("{0} results", r.ToList().Count); + + var odataQuery = Helpers.Build("$filter=Name eq 'parent 61'"); + var parents = this._session.Query(); + + var results = odataQuery.ApplyTo(parents).Cast().ToList(); + Assert.That(results, Has.Count.EqualTo(1)); + } + } } diff --git a/webapi.nhibernate-odata.sln.GhostDoc.xml b/webapi.nhibernate-odata.sln.GhostDoc.xml new file mode 100644 index 0000000..0c987e9 --- /dev/null +++ b/webapi.nhibernate-odata.sln.GhostDoc.xml @@ -0,0 +1,6 @@ + + + *.min.js + jquery*.js + + From fa1acdebedcde9d2741b4b5441d9b77f92c243bd Mon Sep 17 00:00:00 2001 From: cdroulers Date: Thu, 21 May 2015 19:28:59 -0400 Subject: [PATCH 2/5] Make substring(Name, 1) work. Add more test cases as well for other methods which don't need special attention. But we are covering our bases! --- .../Internal/FixStringMethodsVisitor.cs | 13 +++++++---- .../Internal/GivenNHibernateQuery.cs | 23 +++++++------------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs index b6df943..bbcc602 100644 --- a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs +++ b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs @@ -22,6 +22,8 @@ namespace Pathoschild.WebApi.NhibernateOdata.Internal /// == .Constant<System.Nullable`1[System.Boolean]>(True) /// } /// + /// + /// The actual System.Web.Http.OData parser DOES NOT support the "replace" string method, so we can't make it go through NHibernate. /// public class FixStringMethodsVisitor : ExpressionVisitor { @@ -99,14 +101,15 @@ private Expression HandleConditionalExpression(Expression original, ConditionalE if (firstLevelMethodCallExpression != null) { // Using the method name and declaring type as strings because I don't want to add a dependency to the project for a simple check like that. - if (firstLevelMethodCallExpression.Method.Name == "SubstringStartAndLength" && - firstLevelMethodCallExpression.Method.DeclaringType != null && - firstLevelMethodCallExpression.Method.DeclaringType.FullName == "System.Web.Http.OData.Query.Expressions.ClrSafeFunctions") + if (firstLevelMethodCallExpression.Method.DeclaringType != null && + firstLevelMethodCallExpression.Method.DeclaringType.FullName == "System.Web.Http.OData.Query.Expressions.ClrSafeFunctions" && + (firstLevelMethodCallExpression.Method.Name == "SubstringStartAndLength" || firstLevelMethodCallExpression.Method.Name == "SubstringStart")) { + var arguments = firstLevelMethodCallExpression.Arguments.Skip(1).ToArray(); return Expression.Call( firstLevelMethodCallExpression.Arguments[0], - typeof(string).GetMethod("Substring", new[] { typeof(int), typeof(int) }), - firstLevelMethodCallExpression.Arguments.Skip(1).ToArray()); + typeof(string).GetMethod("Substring", arguments.Select(x => typeof(int)).ToArray()), + arguments); } } diff --git a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs index 78fec26..dccd7f7 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -87,6 +87,7 @@ public void When_projecting_one_column_Then_only_queries_one_column() Assert.Inconclusive("Gotta check the output of NHibernate"); } + [TestCase("$filter=Name eq 'parent 61'", 1)] [TestCase("$filter=substringof('parent', Name) eq true", 2)] [TestCase("$filter=substringof('parent', Name) eq true and substringof('61', Name) eq false", 1)] [TestCase("$filter=substringof('parent', Name)", 2)] @@ -97,9 +98,15 @@ public void When_projecting_one_column_Then_only_queries_one_column() [TestCase("$filter=not substringof('wot', Name) and startswith(Name, 'parent 61')", 1)] [TestCase("$filter=startswith(Name, 'parent') eq false", 0)] [TestCase("$filter=endswith(Name, 'parent 61') eq false", 1)] + [TestCase("$filter=substring(Name, 1) eq 'arent 61'", 1)] [TestCase("$filter=substring(Name, 1, 2) eq 'ar'", 2)] [TestCase("$filter=substring(Name, 1, 2) eq 'ar' and startswith(Name, 'par')", 2)] - public void When_filtering_one_column_with_methods_Then_uses_where_like(string filter, int resultCount) + [TestCase("$filter=tolower(Name) eq 'parent 61' and toupper(Name) eq 'PARENT 61'", 1)] + [TestCase("$filter=trim(Name) eq 'parent 61'", 1)] + [TestCase("$filter=length(Name) eq 9", 2)] + //[TestCase("$filter=indexof(Name, '61') eq 6", 1)] + //[TestCase("$filter=concat(Name, 'test') eq 'parent 61test'", 1)] + public void When_filtering_with_string_methods_Then_generates_proper_nhibernate_query(string filter, int resultCount) { var visitor = new FixStringMethodsVisitor(); var odataQuery = Helpers.Build(filter); @@ -109,19 +116,5 @@ public void When_filtering_one_column_with_methods_Then_uses_where_like(string f var results = odataQuery.ApplyTo(parents).Cast().ToList(); Assert.That(results, Has.Count.EqualTo(resultCount)); } - - [Test] - public void When_filtering_one_column_with_eq_Then_uses_where() - { - Console.WriteLine("What it should look like:"); - var r = this._session.Query().Where(x => x.Name == "parent"); - Console.WriteLine("{0} results", r.ToList().Count); - - var odataQuery = Helpers.Build("$filter=Name eq 'parent 61'"); - var parents = this._session.Query(); - - var results = odataQuery.ApplyTo(parents).Cast().ToList(); - Assert.That(results, Has.Count.EqualTo(1)); - } } } From 5fb8d96a19870850985fc213f56ec043c401d7d1 Mon Sep 17 00:00:00 2001 From: cdroulers Date: Mon, 25 May 2015 20:15:08 -0400 Subject: [PATCH 3/5] Add support for "indexof" in OData query. --- .../Internal/FixStringMethodsVisitor.cs | 20 ++++++++++++++++--- .../Internal/GivenNHibernateQuery.cs | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs index bbcc602..303515a 100644 --- a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs +++ b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs @@ -34,8 +34,11 @@ public class FixStringMethodsVisitor : ExpressionVisitor /// This is used to recognize the top-level node for logging. private bool IsRecursing; + /// A list of boolean return methods supported by this visitor. + private readonly List BooleanReturnStringMethods = new List(); + /// A list of methods supported by this visitor. - private readonly List StringMethods = new List(); + private readonly List IntegerStringMethods = new List(); /********* @@ -44,7 +47,8 @@ public class FixStringMethodsVisitor : ExpressionVisitor /// Constructs an instance. public FixStringMethodsVisitor() { - this.StringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Contains" || x.Name == "StartsWith" || x.Name == "EndsWith")); + this.BooleanReturnStringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Contains" || x.Name == "StartsWith" || x.Name == "EndsWith")); + this.IntegerStringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "IndexOf").ToList()); } /// Dispatches the expression to one of the more specialized visit methods in this class. @@ -82,7 +86,7 @@ private Expression HandleConditionalExpression(Expression original, ConditionalE var methodCallExpression = elseExpression.Operand as MethodCallExpression; if (methodCallExpression != null) { - if (this.StringMethods.Contains(methodCallExpression.Method)) + if (this.BooleanReturnStringMethods.Contains(methodCallExpression.Method)) { var methodCallReplacement = Expression.Call( methodCallExpression.Object, @@ -91,7 +95,17 @@ private Expression HandleConditionalExpression(Expression original, ConditionalE // Convert the result to a nullable boolean so the Expression.Equal works. var result = Expression.Convert(methodCallReplacement, typeof(bool?)); + return result; + } + + if (this.IntegerStringMethods.Contains(methodCallExpression.Method)) + { + var methodCallReplacement = Expression.Call( + methodCallExpression.Object, + methodCallExpression.Method, + methodCallExpression.Arguments); + var result = Expression.Convert(methodCallReplacement, typeof(int?)); return result; } } diff --git a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs index dccd7f7..7a09bef 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -104,7 +104,7 @@ public void When_projecting_one_column_Then_only_queries_one_column() [TestCase("$filter=tolower(Name) eq 'parent 61' and toupper(Name) eq 'PARENT 61'", 1)] [TestCase("$filter=trim(Name) eq 'parent 61'", 1)] [TestCase("$filter=length(Name) eq 9", 2)] - //[TestCase("$filter=indexof(Name, '61') eq 6", 1)] + [TestCase("$filter=indexof(Name, '61') eq 8", 1)] //[TestCase("$filter=concat(Name, 'test') eq 'parent 61test'", 1)] public void When_filtering_with_string_methods_Then_generates_proper_nhibernate_query(string filter, int resultCount) { From 5d45131fc623798034bebdaedd4563b8e62a9dfe Mon Sep 17 00:00:00 2001 From: cdroulers Date: Tue, 26 May 2015 12:44:46 -0400 Subject: [PATCH 4/5] Support concat method in OData query! --- .../Internal/FixStringMethodsVisitor.cs | 11 ++++++++++- .../Internal/GivenNHibernateQuery.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs index 303515a..06590fc 100644 --- a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs +++ b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs @@ -37,9 +37,12 @@ public class FixStringMethodsVisitor : ExpressionVisitor /// A list of boolean return methods supported by this visitor. private readonly List BooleanReturnStringMethods = new List(); - /// A list of methods supported by this visitor. + /// A list of integer return methods supported by this visitor. private readonly List IntegerStringMethods = new List(); + /// A list of concatenation methods supported by this visitor. + private readonly List ConcatStringMethods = new List(); + /********* ** Public methods @@ -49,6 +52,7 @@ public FixStringMethodsVisitor() { this.BooleanReturnStringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Contains" || x.Name == "StartsWith" || x.Name == "EndsWith")); this.IntegerStringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "IndexOf").ToList()); + this.ConcatStringMethods.AddRange(typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Static).Where(x => x.Name == "Concat").ToList()); } /// Dispatches the expression to one of the more specialized visit methods in this class. @@ -125,6 +129,11 @@ private Expression HandleConditionalExpression(Expression original, ConditionalE typeof(string).GetMethod("Substring", arguments.Select(x => typeof(int)).ToArray()), arguments); } + + if (this.ConcatStringMethods.Contains(firstLevelMethodCallExpression.Method)) + { + return Expression.Add(firstLevelMethodCallExpression.Arguments.First(), firstLevelMethodCallExpression.Arguments.Last(), firstLevelMethodCallExpression.Method); + } } return original; diff --git a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs index 7a09bef..1bc7adc 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -105,7 +105,7 @@ public void When_projecting_one_column_Then_only_queries_one_column() [TestCase("$filter=trim(Name) eq 'parent 61'", 1)] [TestCase("$filter=length(Name) eq 9", 2)] [TestCase("$filter=indexof(Name, '61') eq 8", 1)] - //[TestCase("$filter=concat(Name, 'test') eq 'parent 61test'", 1)] + [TestCase("$filter=concat(Name, 'test') eq 'parent 61test'", 1)] public void When_filtering_with_string_methods_Then_generates_proper_nhibernate_query(string filter, int resultCount) { var visitor = new FixStringMethodsVisitor(); From f86207724337fd46d16730b9b0ee578429ee7788 Mon Sep 17 00:00:00 2001 From: cdroulers Date: Wed, 27 May 2015 21:57:22 -0400 Subject: [PATCH 5/5] Add notes and more columns for test cases --- .../Internal/GivenNHibernateQuery.cs | 7 ++++- .../Mappings/ParentMap.cs | 6 ++-- .../20150321104800_InitialMigration.cs | 6 ++-- .../Migrations/TestProfile.cs | 28 ++++++++++--------- .../Models/Parent.cs | 9 ++++-- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs index 1bc7adc..50e174d 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -70,12 +70,16 @@ public void When_expanding_children_Then_works() [Test] public void When_projecting_one_column_Then_only_queries_one_column() { + var visitor = new FixStringMethodsVisitor(); Console.WriteLine("What it should look like:"); var query = this._session.Query().Select(x => new { x.Name }).ToList(); Console.WriteLine("{0} results", query.Count); - var odataQuery = Helpers.Build("$select=Name"); + // The Expression output is some recursive thing (PropertyContainer+NamedPropertyWithNext). + // I think it might be easier to remove the current select node and rebuild with the actual ODataOptions directly. + var odataQuery = Helpers.Build("$select=Id,Name,CreatedOn,Value"); var parents = this._session.Query(); + parents = parents.InterceptWith(visitor); var results = odataQuery.ApplyTo(parents).Cast(); @@ -106,6 +110,7 @@ public void When_projecting_one_column_Then_only_queries_one_column() [TestCase("$filter=length(Name) eq 9", 2)] [TestCase("$filter=indexof(Name, '61') eq 8", 1)] [TestCase("$filter=concat(Name, 'test') eq 'parent 61test'", 1)] + //[TestCase("$filter=toupper(substring(Name, 1, 2)) eq 'AR'", 2)] public void When_filtering_with_string_methods_Then_generates_proper_nhibernate_query(string filter, int resultCount) { var visitor = new FixStringMethodsVisitor(); diff --git a/WebApi.NHibernate-OData.Tests/Mappings/ParentMap.cs b/WebApi.NHibernate-OData.Tests/Mappings/ParentMap.cs index 0048e21..b94da94 100644 --- a/WebApi.NHibernate-OData.Tests/Mappings/ParentMap.cs +++ b/WebApi.NHibernate-OData.Tests/Mappings/ParentMap.cs @@ -8,8 +8,10 @@ public class ParentMap : ClassMap public ParentMap() { this.Table("Parents"); - this.Id(x => x.Id); - this.Map(x => x.Name); + this.Id(x => x.Id); + this.Map(x => x.Name); + this.Map(x => x.CreatedOn); + this.Map(x => x.Value); this.HasMany(x => x.Children) .KeyColumn("ParentId") diff --git a/WebApi.NHibernate-OData.Tests/Migrations/20150321104800_InitialMigration.cs b/WebApi.NHibernate-OData.Tests/Migrations/20150321104800_InitialMigration.cs index c217e40..5749b0c 100644 --- a/WebApi.NHibernate-OData.Tests/Migrations/20150321104800_InitialMigration.cs +++ b/WebApi.NHibernate-OData.Tests/Migrations/20150321104800_InitialMigration.cs @@ -8,8 +8,10 @@ public class InitialMigration : Migration public override void Up() { this.Create.Table("Parents") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey() - .WithColumn("Name").AsAnsiString(50).NotNullable(); + .WithColumn("Id").AsInt32().NotNullable().PrimaryKey() + .WithColumn("Name").AsAnsiString(50).NotNullable() + .WithColumn("CreatedOn").AsDate().NotNullable() + .WithColumn("Value").AsDecimal(18,2).NotNullable(); this.Create.Table("Children") .WithColumn("Id").AsInt32().NotNullable().PrimaryKey() diff --git a/WebApi.NHibernate-OData.Tests/Migrations/TestProfile.cs b/WebApi.NHibernate-OData.Tests/Migrations/TestProfile.cs index 6ce260e..a51b547 100644 --- a/WebApi.NHibernate-OData.Tests/Migrations/TestProfile.cs +++ b/WebApi.NHibernate-OData.Tests/Migrations/TestProfile.cs @@ -1,18 +1,20 @@ -using FluentMigrator; +using System; + +using FluentMigrator; namespace Pathoschild.WebApi.NhibernateOdata.Tests.Migrations { - [Profile("Default")] - public class TestProfile : ForwardOnlyMigration - { - public override void Up() - { - this.Insert.IntoTable("Parents") - .Row(new { Id = 61, Name = "parent 61" }) - .Row(new { Id = 63, Name = "parent 63" }); + [Profile("Default")] + public class TestProfile : ForwardOnlyMigration + { + public override void Up() + { + this.Insert.IntoTable("Parents") + .Row(new { Id = 61, Name = "parent 61", CreatedOn = new DateTime(2015, 1, 1), Value = 15.15m }) + .Row(new { Id = 63, Name = "parent 63", CreatedOn = new DateTime(2014, 1, 2), Value = 45.15m }); - this.Insert.IntoTable("Children") - .Row(new { Id = 11, Name = "child 11", ParentId = 61 }); - } - } + this.Insert.IntoTable("Children") + .Row(new { Id = 11, Name = "child 11", ParentId = 61 }); + } + } } diff --git a/WebApi.NHibernate-OData.Tests/Models/Parent.cs b/WebApi.NHibernate-OData.Tests/Models/Parent.cs index d26de0c..4495d1b 100644 --- a/WebApi.NHibernate-OData.Tests/Models/Parent.cs +++ b/WebApi.NHibernate-OData.Tests/Models/Parent.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; namespace Pathoschild.WebApi.NhibernateOdata.Tests.Models @@ -8,7 +9,11 @@ public class Parent { public virtual int Id { get; set; } - public virtual string Name { get; set; } + public virtual string Name { get; set; } + + public virtual DateTime CreatedOn { get; set; } + + public virtual decimal Value { get; set; } public virtual IList Children { get; set; }