diff --git a/src/CHANGELOG.TXT b/src/CHANGELOG.TXT index ffc9a37..0469eee 100644 --- a/src/CHANGELOG.TXT +++ b/src/CHANGELOG.TXT @@ -46,3 +46,4 @@ 2.8.1: Fixed case problem when checking for Devart Oracle provider 2.9: Added filter options to turn off applying filters to child properties or to prevent them from being applied recursively 2.9.1: Changed Oracle version check to use 'product_component_version' instead of 'v$instance' which is restricted to admins +2.10: Added support for string.Contains and .EndsWith diff --git a/src/DynamicFiltersTests/ChildCollectionFiltersTests.cs b/src/DynamicFiltersTests/ChildCollectionFiltersTests.cs index 75bbc2d..1b26abd 100644 --- a/src/DynamicFiltersTests/ChildCollectionFiltersTests.cs +++ b/src/DynamicFiltersTests/ChildCollectionFiltersTests.cs @@ -96,6 +96,17 @@ public void ChildCollection_ManyToManyLoadGeneratedTable() } } + // Tests issue #117 + [TestMethod] + public void ChildCollection_AnyContains() + { + using (var context = new TestContext()) + { + var list = context.EntityISet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 1))); + } + } + #region Models public class EntityBase @@ -211,6 +222,19 @@ public EntityH() public bool IsDeleted { get; set; } } + public class EntityI : EntityBase + { + public ICollection Children { get; set; } + } + + public class EntityIChild : EntityBase + { + public int ParentID { get; set; } + public EntityI Parent { get; set; } + + public string ChildValue { get; set; } + } + #endregion #region TestContext @@ -231,6 +255,8 @@ public class TestContext : TestContextBase, ITestContext public DbSet EntityFChildSet { get; set; } public DbSet EntityGSet { get; set; } public DbSet EntityHSet { get; set; } + public DbSet EntityISet { get; set; } + public DbSet EntityIChildSet { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { @@ -266,6 +292,8 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) modelBuilder.Filter("ISoftDeleteFilter", (ISoftDelete d) => d.IsDeleted, false); + modelBuilder.Filter("EntityIFilter", (EntityI i, string val) => i.Children.Any(c => c.ChildValue.Contains(val)), () => "23"); + // TODO: Count() // TODO: Count([predicate]) // TODO: Where([predicate]) @@ -362,6 +390,25 @@ public override void Seed() } }); + EntityISet.Add(new EntityI + { + ID = 1, + Children = new List + { + new EntityIChild { ID = 1, ChildValue = "1234" }, + new EntityIChild { ID = 2, ChildValue = "5678" }, + } + }); + EntityISet.Add(new EntityI + { + ID = 2, + Children = new List + { + new EntityIChild { ID = 3, ChildValue = "abcd" }, + new EntityIChild { ID = 4, ChildValue = "edfg" }, + } + }); + SaveChanges(); } } diff --git a/src/DynamicFiltersTests/DynamicFiltersTests.csproj b/src/DynamicFiltersTests/DynamicFiltersTests.csproj index 050faf5..38bb86a 100644 --- a/src/DynamicFiltersTests/DynamicFiltersTests.csproj +++ b/src/DynamicFiltersTests/DynamicFiltersTests.csproj @@ -110,7 +110,7 @@ - + diff --git a/src/DynamicFiltersTests/StartsWithTests.cs b/src/DynamicFiltersTests/StartsWithTests.cs deleted file mode 100644 index ee1773d..0000000 --- a/src/DynamicFiltersTests/StartsWithTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Data.Entity; -using System.Linq; -using EntityFramework.DynamicFilters; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace DynamicFiltersTests -{ - // Tests related to Contains() operator in lambda filters (issue #13 - [TestClass] - public class StartsWithTests - { - [TestMethod] - public void StartsWith_ConstantValue() - { - using (var context1 = new TestContext()) - { - var list = context1.EntityASet.ToList(); - Assert.IsTrue((list.Count == 2) && list.All(a => (a.ID == 1) || (a.ID == 2))); - } - } - - [TestMethod] - public void StartsWith_ParameterValue() - { - using (var context1 = new TestContext()) - { - try - { - var list = context1.EntityBSet.ToList(); - Assert.IsTrue((list.Count == 2) && list.All(a => (a.ID == 3) || (a.ID == 4))); - } - catch (Exception ex) - { - // A System.Format exception is the expected result for SQL Server CE. It does not support - // "like @value+'%'". See: https://stackoverflow.com/questions/1916248/how-to-use-parameter-with-like-in-sql-server-compact-edition - // And there is no way for us to know that we need to append the % character to the parameter value during - // sql interception (because we don't know that the param is being used on a StartsWith function). - if ((ex.InnerException != null) && (ex.InnerException is FormatException) && context1.IsSQLCE()) - return; - - throw ex; - } - } - } - - [TestMethod] - public void StartsWith_ConstantSource() - { - using (var context1 = new TestContext()) - { - var list = context1.EntityCSet.ToList(); - Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 2))); - } - } - - [TestMethod] - public void StartsWith_ParameterSource() - { - using (var context1 = new TestContext()) - { - var list = context1.EntityDSet.ToList(); - Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 5))); - } - } - - #region Models - - public abstract class EntityBase - { - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int ID { get; set; } - - public string Name { get; set; } - } - - public class EntityA : EntityBase { } - public class EntityB : EntityBase { } - public class EntityC : EntityBase { } - public class EntityD : EntityBase { } - - #endregion - - #region TestContext - - public class TestContext : TestContextBase, ITestContext - { - public DbSet EntityASet { get; set; } - public DbSet EntityBSet { get; set; } - public DbSet EntityCSet { get; set; } - public DbSet EntityDSet { get; set; } - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Filter("EntityAFilter", (EntityA a) => a.Name.StartsWith("J")); - modelBuilder.Filter("EntityBFilter", (EntityB b, string val) => b.Name.StartsWith(val), () => "B"); - modelBuilder.Filter("EntityCFilter", (EntityC c) => "Joeseph".StartsWith(c.Name)); - modelBuilder.Filter("EntityDFilter", (EntityD d, string val) => val.StartsWith(d.Name), () => "Frederick"); - } - - public override void Seed() - { - System.Diagnostics.Debug.Print("Seeding db"); - - var names = new string[] { "John", "Joe", "Bob", "Barney", "Fred" }; - - for (int i = 0; i < 5; i++) - { - EntityASet.Add(new EntityA { ID = i + 1, Name = names[i] }); - EntityBSet.Add(new EntityB { ID = i + 1, Name = names[i] }); - EntityCSet.Add(new EntityC { ID = i + 1, Name = names[i] }); - EntityDSet.Add(new EntityD { ID = i + 1, Name = names[i] }); - } - - SaveChanges(); - } - } - - #endregion - } - - -} diff --git a/src/DynamicFiltersTests/StringFunctionsTests.cs b/src/DynamicFiltersTests/StringFunctionsTests.cs new file mode 100644 index 0000000..100a6cc --- /dev/null +++ b/src/DynamicFiltersTests/StringFunctionsTests.cs @@ -0,0 +1,243 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data.Entity; +using System.Linq; +using EntityFramework.DynamicFilters; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DynamicFiltersTests +{ + // Tests related to Contains() operator in lambda filters (issue #13 + [TestClass] + public class StringFunctionsTests + { + [TestMethod] + public void StringFunction_StartsWith_ConstantValue() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityASet.ToList(); + Assert.IsTrue((list.Count == 2) && list.All(a => (a.ID == 1) || (a.ID == 2))); + } + } + + [TestMethod] + public void StringFunction_StartsWith_ParameterValue() + { + using (var context1 = new TestContext()) + { + try + { + var list = context1.EntityBSet.ToList(); + Assert.IsTrue((list.Count == 2) && list.All(a => (a.ID == 3) || (a.ID == 4))); + } + catch (Exception ex) + { + // A System.Format exception is the expected result for SQL Server CE. It does not support + // "like @value+'%'". See: https://stackoverflow.com/questions/1916248/how-to-use-parameter-with-like-in-sql-server-compact-edition + // And there is no way for us to know that we need to append the % character to the parameter value during + // sql interception (because we don't know that the param is being used on a StartsWith function). + if ((ex.InnerException != null) && (ex.InnerException is FormatException) && context1.IsSQLCE()) + return; + + throw ex; + } + } + } + + [TestMethod] + public void StringFunction_StartsWith_ConstantSource() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityCSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 2))); + } + } + + [TestMethod] + public void StringFunction_StartsWith_ParameterSource() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityDSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 5))); + } + } + + [TestMethod] + public void StringFunction_Contains1() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityESet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_Contains2() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityFSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_Contains3() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityGSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_Contains4() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityHSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_EndsWith1() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityISet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_EndsWith2() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityJSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_EndsWith3() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityKSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + [TestMethod] + public void StringFunction_EndsWith4() + { + using (var context1 = new TestContext()) + { + var list = context1.EntityLSet.ToList(); + Assert.IsTrue((list.Count == 1) && list.All(a => (a.ID == 4))); + } + } + + #region Models + + public abstract class EntityBase + { + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int ID { get; set; } + + public string Name { get; set; } + } + + public class EntityA : EntityBase { } + public class EntityB : EntityBase { } + public class EntityC : EntityBase { } + public class EntityD : EntityBase { } + public class EntityE : EntityBase { } + public class EntityF : EntityBase { } + public class EntityG : EntityBase { } + public class EntityH : EntityBase { } + public class EntityI : EntityBase { } + public class EntityJ : EntityBase { } + public class EntityK : EntityBase { } + public class EntityL : EntityBase { } + + #endregion + + #region TestContext + + public class TestContext : TestContextBase, ITestContext + { + public DbSet EntityASet { get; set; } + public DbSet EntityBSet { get; set; } + public DbSet EntityCSet { get; set; } + public DbSet EntityDSet { get; set; } + public DbSet EntityESet { get; set; } + public DbSet EntityFSet { get; set; } + public DbSet EntityGSet { get; set; } + public DbSet EntityHSet { get; set; } + public DbSet EntityISet { get; set; } + public DbSet EntityJSet { get; set; } + public DbSet EntityKSet { get; set; } + public DbSet EntityLSet { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Filter("EntityAFilter", (EntityA a) => a.Name.StartsWith("J")); + modelBuilder.Filter("EntityBFilter", (EntityB b, string val) => b.Name.StartsWith(val), () => "B"); + modelBuilder.Filter("EntityCFilter", (EntityC c) => "Joeseph".StartsWith(c.Name)); + modelBuilder.Filter("EntityDFilter", (EntityD d, string val) => val.StartsWith(d.Name), () => "Frederick"); + + modelBuilder.Filter("EntityEFilter", (EntityE e) => e.Name.Contains("bar")); + modelBuilder.Filter("EntityFFilter", (EntityF f, string val) => f.Name.Contains(val), () => "bar"); + modelBuilder.Filter("EntityGFilter", (EntityG g) => "barney rubble".Contains(g.Name)); + modelBuilder.Filter("EntityHFilter", (EntityH h, string val) => val.Contains(h.Name), () => "barney rubble"); + + modelBuilder.Filter("EntityIFilter", (EntityI i) => i.Name.EndsWith("ney")); + modelBuilder.Filter("EntityJFilter", (EntityJ j, string val) => j.Name.EndsWith(val), () => "ney"); + modelBuilder.Filter("EntityKFilter", (EntityK k) => "rubble, barney".EndsWith(k.Name)); + modelBuilder.Filter("EntityLFilter", (EntityL l, string val) => val.Contains(l.Name), () => "rubble, barney"); + } + + public override void Seed() + { + System.Diagnostics.Debug.Print("Seeding db"); + + var names = new string[] { "John", "Joe", "Bob", "Barney", "Fred" }; + + for (int i = 0; i < 5; i++) + { + EntityASet.Add(new EntityA { ID = i + 1, Name = names[i] }); + EntityBSet.Add(new EntityB { ID = i + 1, Name = names[i] }); + EntityCSet.Add(new EntityC { ID = i + 1, Name = names[i] }); + EntityDSet.Add(new EntityD { ID = i + 1, Name = names[i] }); + EntityESet.Add(new EntityE { ID = i + 1, Name = names[i] }); + EntityFSet.Add(new EntityF { ID = i + 1, Name = names[i] }); + EntityGSet.Add(new EntityG { ID = i + 1, Name = names[i] }); + EntityHSet.Add(new EntityH { ID = i + 1, Name = names[i] }); + EntityISet.Add(new EntityI { ID = i + 1, Name = names[i] }); + EntityJSet.Add(new EntityJ { ID = i + 1, Name = names[i] }); + EntityKSet.Add(new EntityK { ID = i + 1, Name = names[i] }); + EntityLSet.Add(new EntityL { ID = i + 1, Name = names[i] }); + } + + SaveChanges(); + } + } + + #endregion + } + + +} diff --git a/src/EntityFramework.DynamicFilters/LambdaToDbExpressionVisitor.cs b/src/EntityFramework.DynamicFilters/LambdaToDbExpressionVisitor.cs index 5664dd1..f19ec67 100644 --- a/src/EntityFramework.DynamicFilters/LambdaToDbExpressionVisitor.cs +++ b/src/EntityFramework.DynamicFilters/LambdaToDbExpressionVisitor.cs @@ -491,10 +491,16 @@ protected override Expression VisitMethodCall(MethodCallExpression node) switch (node.Method.Name) { case "Contains": - expression = MapContainsExpression(node); + if (node.Method.DeclaringType == typeof(string)) + expression = MapStringLikeExpression(node, false, false); + else + expression = MapEnumerableContainsExpression(node); break; case "StartsWith": - expression = MapStartsWithExpression(node); + expression = MapStringLikeExpression(node, true, false); + break; + case "EndsWith": + expression = MapStringLikeExpression(node, false, true); break; case "Any": case "All": @@ -515,7 +521,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return expression; } - private Expression MapContainsExpression(MethodCallExpression node) + private Expression MapEnumerableContainsExpression(MethodCallExpression node) { var expression = base.VisitMethodCall(node) as MethodCallExpression; @@ -625,7 +631,7 @@ private bool SupportsIn() return !entityConnection.StoreConnection.GetType().FullName.Contains("Oracle"); } - private Expression MapStartsWithExpression(MethodCallExpression node) + private Expression MapStringLikeExpression(MethodCallExpression node, bool matchStart, bool matchEnd) { var expression = base.VisitMethodCall(node) as MethodCallExpression; @@ -642,7 +648,12 @@ private Expression MapStartsWithExpression(MethodCallExpression node) if ((constantExpression == null) || (constantExpression.Value == null)) throw new NullReferenceException("Parameter to StartsWith cannot be null"); - dbExpression = DbExpressionBuilder.Like(srcExpression, DbExpressionBuilder.Constant(constantExpression.Value.ToString() + "%")); + string value = matchStart ? "" : "%"; + value += constantExpression.Value.ToString(); + if (!matchEnd) + value += "%"; + + dbExpression = DbExpressionBuilder.Like(srcExpression, DbExpressionBuilder.Constant(value)); } else { @@ -652,7 +663,11 @@ private Expression MapStartsWithExpression(MethodCallExpression node) // It works but generates some crazy conditions using charindex which I don't think will use indexes as well as "like"... //dbExpression = DbExpressionBuilder.Equal(DbExpressionBuilder.True, srcExpression.StartsWith(argExpression)); - dbExpression = DbExpressionBuilder.Like(srcExpression, argExpression.Concat(DbExpressionBuilder.Constant("%"))); + DbExpression value = matchStart ? argExpression : DbExpressionBuilder.Constant("%").Concat(argExpression); + if (!matchEnd) + value = value.Concat(DbExpressionBuilder.Constant("%")); + + dbExpression = DbExpressionBuilder.Like(srcExpression, value); } MapExpressionToDbExpression(expression, dbExpression); diff --git a/src/EntityFramework.DynamicFilters/Properties/AssemblyInfo.cs b/src/EntityFramework.DynamicFilters/Properties/AssemblyInfo.cs index 2d1c4c8..b9e8e57 100644 --- a/src/EntityFramework.DynamicFilters/Properties/AssemblyInfo.cs +++ b/src/EntityFramework.DynamicFilters/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.9.1")] -[assembly: AssemblyFileVersion("2.9.1")] -[assembly: AssemblyInformationalVersion("2.9.1")] +[assembly: AssemblyVersion("2.10.0")] +[assembly: AssemblyFileVersion("2.10.0")] +[assembly: AssemblyInformationalVersion("2.10.0")]