diff --git a/README.md b/README.md index fd0b677..cbd4efa 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,15 @@ public class SieveCustomSortMethods : ISieveCustomSortMethods return result; // Must return modified IQueryable } + + public IQueryable Oldest(IQueryable source, bool useThenBy, bool desc) where T : BaseEntity // Generic functions are allowed too + { + var result = useThenBy ? + ((IOrderedQueryable)source).ThenByDescending(p => p.DateCreated) : + source.OrderByDescending(p => p.DateCreated); + + return result; + } } ``` And `SieveCustomFilterMethods`: @@ -97,6 +106,12 @@ public class SieveCustomFilterMethods : ISieveCustomFilterMethods return result; // Must return modified IQueryable } + + public IQueryable Latest(IQueryable source, string op, string[] values) where T : BaseEntity // Generic functions are allowed too + { + var result = source.Where(c => c.DateCreated > DateTimeOffset.UtcNow.AddDays(-14)); + return result; + } } ``` @@ -192,6 +207,7 @@ You can replace this DSL with your own (eg. use JSON instead) by implementing an | `@=*` | Case-insensitive string Contains | | `_=*` | Case-insensitive string Starts with | | `==*` | Case-insensitive string Equals | +| `!=*` | Case-insensitive string Not equals | | `!@=*` | Case-insensitive string does not Contains | | `!_=*` | Case-insensitive string does not Starts with | diff --git a/Sieve/Models/FilterTerm.cs b/Sieve/Models/FilterTerm.cs index 69395ed..77bfeec 100644 --- a/Sieve/Models/FilterTerm.cs +++ b/Sieve/Models/FilterTerm.cs @@ -13,6 +13,7 @@ namespace Sieve.Models private static readonly string[] Operators = new string[] { "!@=*", "!_=*", + "!=*", "!@=", "!_=", "==*", diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index cec6bdf..58b9267 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -186,6 +186,8 @@ namespace Sieve.Services { propertyValue = Expression.PropertyOrField(propertyValue, part); } + + if (filterTerm.Values == null) continue; foreach (var filterTermValue in filterTerm.Values) { @@ -335,10 +337,9 @@ namespace Sieve.Services var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize; - result = result.Skip((page - 1) * pageSize); - if (pageSize > 0) { + result = result.Skip((page - 1) * pageSize); result = result.Take(Math.Min(pageSize, maxPageSize)); } @@ -387,6 +388,28 @@ namespace Sieve.Services _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, typeof(IQueryable)); + + if (customMethod == null) + { + // Find generic methods `public IQueryable Filter(IQueryable source, ...)` + var genericCustomMethod = parent?.GetType() + .GetMethodExt(name, + _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, + typeof(IQueryable<>)); + + if (genericCustomMethod != null && + genericCustomMethod.ReturnType.IsGenericType && + genericCustomMethod.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + { + var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0]; + var constraints = genericBaseType.GetGenericParameterConstraints(); + if (constraints == null || constraints.Length == 0 || constraints.All((t) => t.IsAssignableFrom(typeof(TEntity)))) + { + customMethod = genericCustomMethod.MakeGenericMethod(typeof(TEntity)); + } + } + } + if (customMethod != null) { try diff --git a/SieveUnitTests/Entities/BaseEntity.cs b/SieveUnitTests/Entities/BaseEntity.cs new file mode 100644 index 0000000..1ea6697 --- /dev/null +++ b/SieveUnitTests/Entities/BaseEntity.cs @@ -0,0 +1,13 @@ +using System; +using Sieve.Attributes; + +namespace SieveUnitTests.Entities +{ + public class BaseEntity + { + public int Id { get; set; } + + [Sieve(CanFilter = true, CanSort = true)] + public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; + } +} diff --git a/SieveUnitTests/Entities/Comment.cs b/SieveUnitTests/Entities/Comment.cs index e3418be..0e67563 100644 --- a/SieveUnitTests/Entities/Comment.cs +++ b/SieveUnitTests/Entities/Comment.cs @@ -3,13 +3,8 @@ using Sieve.Attributes; namespace SieveUnitTests.Entities { - public class Comment + public class Comment : BaseEntity { - public int Id { get; set; } - - [Sieve(CanFilter = true, CanSort = true)] - public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; - [Sieve(CanFilter = true)] public string Text { get; set; } } diff --git a/SieveUnitTests/Entities/Post.cs b/SieveUnitTests/Entities/Post.cs index 20ecdfc..1503486 100644 --- a/SieveUnitTests/Entities/Post.cs +++ b/SieveUnitTests/Entities/Post.cs @@ -3,9 +3,8 @@ using Sieve.Attributes; namespace SieveUnitTests.Entities { - public class Post + public class Post : BaseEntity { - public int Id { get; set; } [Sieve(CanFilter = true, CanSort = true)] public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 8); @@ -16,9 +15,6 @@ namespace SieveUnitTests.Entities [Sieve(CanFilter = true, CanSort = true)] public int CommentCount { get; set; } = new Random().Next(0, 1000); - [Sieve(CanFilter = true, CanSort = true)] - public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; - [Sieve(CanFilter = true, CanSort = true)] public int? CategoryId { get; set; } = new Random().Next(0, 4); diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 471b216..58eb50a 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -96,6 +96,20 @@ namespace SieveUnitTests Assert.IsTrue(result.Count() == 1); } + [TestMethod] + public void NotEqualsCanBeCaseInsensitive() + { + var model = new SieveModel() + { + Filters = "Title!=*a" + }; + + var result = _processor.Apply(model, _posts); + + Assert.AreEqual(result.First().Id, 1); + Assert.IsTrue(result.Count() == 3); + } + [TestMethod] public void ContainsIsCaseSensitive() { @@ -193,6 +207,20 @@ namespace SieveUnitTests Assert.IsTrue(result.Count() == 3); } + [TestMethod] + public void CustomGenericFiltersWork() + { + var model = new SieveModel() + { + Filters = "Latest", + }; + + var result = _processor.Apply(model, _comments); + + Assert.IsFalse(result.Any(p => p.Id == 0)); + Assert.IsTrue(result.Count() == 2); + } + [TestMethod] public void CustomFiltersWithOperatorsWork() { @@ -272,6 +300,19 @@ namespace SieveUnitTests Assert.IsFalse(result.First().Id == 0); } + [TestMethod] + public void CustomGenericSortsWork() + { + var model = new SieveModel() + { + Sorts = "Oldest", + }; + + var result = _processor.Apply(model, _posts); + + Assert.IsTrue(result.Last().Id == 0); + } + [TestMethod] public void MethodNotFoundExceptionWork() { diff --git a/SieveUnitTests/Services/SieveCustomFilterMethods.cs b/SieveUnitTests/Services/SieveCustomFilterMethods.cs index 68a6132..5b6eecc 100644 --- a/SieveUnitTests/Services/SieveCustomFilterMethods.cs +++ b/SieveUnitTests/Services/SieveCustomFilterMethods.cs @@ -32,5 +32,11 @@ namespace SieveUnitTests.Services { return source; } + + public IQueryable Latest(IQueryable source, string op, string[] values) where T : BaseEntity + { + var result = source.Where(c => c.DateCreated > DateTimeOffset.UtcNow.AddDays(-14)); + return result; + } } } diff --git a/SieveUnitTests/Services/SieveCustomSortMethods.cs b/SieveUnitTests/Services/SieveCustomSortMethods.cs index 8fd3f17..12bdb7f 100644 --- a/SieveUnitTests/Services/SieveCustomSortMethods.cs +++ b/SieveUnitTests/Services/SieveCustomSortMethods.cs @@ -16,5 +16,14 @@ namespace SieveUnitTests.Services return result; } + + public IQueryable Oldest(IQueryable source, bool useThenBy, bool desc) where T : BaseEntity + { + var result = useThenBy ? + ((IOrderedQueryable)source).ThenByDescending(p => p.DateCreated) : + source.OrderByDescending(p => p.DateCreated); + + return result; + } } }