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..06590fc 100644 --- a/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs +++ b/WebAPI.NHibernate-OData/Internal/FixStringMethodsVisitor.cs @@ -5,97 +5,138 @@ 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; - - /// 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")); - } - - /// 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); - - 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); - - // Convert the result to a nullable boolean so the Expression.Equal works. - var result = Expression.Convert(methodCallReplacement, typeof(bool?)); - - return result; - } - } - } - - return original; - } - } + /// 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) + /// } + /// + /// + /// 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 + { + /********* + ** 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 boolean return methods supported by this visitor. + private readonly List BooleanReturnStringMethods = new List(); + + /// 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 + *********/ + /// Constructs an instance. + 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. + /// 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); + + 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.BooleanReturnStringMethods.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?)); + 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; + } + } + } + + 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.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", 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; + } + } } \ 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..50e174d 100644 --- a/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs +++ b/WebApi.NHibernate-OData.Tests/Internal/GivenNHibernateQuery.cs @@ -11,121 +11,115 @@ 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() + { + 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); + + // 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(); + + 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=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)] - [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) 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)] + [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 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(); + 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)); + } + } } 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; } 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 + +