mirror of
https://github.com/Biarity/Sieve.git
synced 2025-08-17 22:55:32 +02:00
* Migrate tests to xunit
* Update sample project to dotnetcore3.1 * Use Sqlite in sample project to run it everywhere * Fix: Filter with escaped comma * Fix: Filter "null" does not work with Contains or StartsWith * Code cleanup: Adjust namespaces, adjust usings
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
@@ -13,7 +14,15 @@ namespace Sieve.Models
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern used to split filters and sorts by comma.
|
||||
/// </summary>
|
||||
private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),\s*";
|
||||
|
||||
/// <summary>
|
||||
/// Escaped comma e.g. used in filter filter string.
|
||||
/// </summary>
|
||||
private const string EscapedComma = @"\,";
|
||||
|
||||
[DataMember]
|
||||
public string Filters { get; set; }
|
||||
@@ -34,15 +43,20 @@ namespace Sieve.Models
|
||||
var value = new List<TFilterTerm>();
|
||||
foreach (var filter in Regex.Split(Filters, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter)) continue;
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filterValue = filter.Replace(EscapedComma, ",");
|
||||
|
||||
if (filter.StartsWith("("))
|
||||
{
|
||||
var filterOpAndVal = filter.Substring(filter.LastIndexOf(")") + 1);
|
||||
var subfilters = filter.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", "");
|
||||
var filterOpAndVal = filterValue[(filterValue.LastIndexOf(")", StringComparison.Ordinal) + 1)..];
|
||||
var subFilters = filterValue.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", "");
|
||||
var filterTerm = new TFilterTerm
|
||||
{
|
||||
Filter = subfilters + filterOpAndVal
|
||||
Filter = subFilters + filterOpAndVal
|
||||
};
|
||||
value.Add(filterTerm);
|
||||
}
|
||||
@@ -50,7 +64,7 @@ namespace Sieve.Models
|
||||
{
|
||||
var filterTerm = new TFilterTerm
|
||||
{
|
||||
Filter = filter
|
||||
Filter = filterValue
|
||||
};
|
||||
value.Add(filterTerm);
|
||||
}
|
||||
@@ -65,29 +79,28 @@ namespace Sieve.Models
|
||||
|
||||
public List<TSortTerm> GetSortsParsed()
|
||||
{
|
||||
if (Sorts != null)
|
||||
{
|
||||
var value = new List<TSortTerm>();
|
||||
foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sort)) continue;
|
||||
|
||||
var sortTerm = new TSortTerm()
|
||||
{
|
||||
Sort = sort
|
||||
};
|
||||
if (!value.Any(s => s.Name == sortTerm.Name))
|
||||
{
|
||||
value.Add(sortTerm);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
else
|
||||
if (Sorts == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = new List<TSortTerm>();
|
||||
foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sort)) continue;
|
||||
|
||||
var sortTerm = new TSortTerm
|
||||
{
|
||||
Sort = sort
|
||||
};
|
||||
|
||||
if (value.All(s => s.Name != sortTerm.Name))
|
||||
{
|
||||
value.Add(sortTerm);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,40 +14,50 @@ namespace Sieve.Services
|
||||
{
|
||||
public class SieveProcessor : SieveProcessor<SieveModel, FilterTerm, SortTerm>, ISieveProcessor
|
||||
{
|
||||
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
|
||||
: base(options, customSortMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class SieveProcessor<TFilterTerm, TSortTerm> : SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
|
||||
public class SieveProcessor<TFilterTerm, TSortTerm> :
|
||||
SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
|
||||
: base(options, customSortMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -57,17 +67,17 @@ namespace Sieve.Services
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
private const string nullFilterValue = "null";
|
||||
private const string NullFilterValue = "null";
|
||||
private readonly IOptions<SieveOptions> _options;
|
||||
private readonly ISieveCustomSortMethods _customSortMethods;
|
||||
private readonly ISieveCustomFilterMethods _customFilterMethods;
|
||||
private readonly SievePropertyMapper mapper = new SievePropertyMapper();
|
||||
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_mapper = MapProperties(_mapper);
|
||||
_options = options;
|
||||
_customSortMethods = customSortMethods;
|
||||
_customFilterMethods = customFilterMethods;
|
||||
@@ -76,7 +86,7 @@ namespace Sieve.Services
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_mapper = MapProperties(_mapper);
|
||||
_options = options;
|
||||
_customSortMethods = customSortMethods;
|
||||
}
|
||||
@@ -84,14 +94,14 @@ namespace Sieve.Services
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_mapper = MapProperties(_mapper);
|
||||
_options = options;
|
||||
_customFilterMethods = customFilterMethods;
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_mapper = MapProperties(_mapper);
|
||||
_options = options;
|
||||
}
|
||||
|
||||
@@ -106,12 +116,8 @@ namespace Sieve.Services
|
||||
/// <param name="applySorting">Should the data be sorted? Defaults to true.</param>
|
||||
/// <param name="applyPagination">Should the data be paginated? Defaults to true.</param>
|
||||
/// <returns>Returns a transformed version of `source`</returns>
|
||||
public IQueryable<TEntity> Apply<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> source,
|
||||
object[] dataForCustomMethods = null,
|
||||
bool applyFiltering = true,
|
||||
bool applySorting = true,
|
||||
public IQueryable<TEntity> Apply<TEntity>(TSieveModel model, IQueryable<TEntity> source,
|
||||
object[] dataForCustomMethods = null, bool applyFiltering = true, bool applySorting = true,
|
||||
bool applyPagination = true)
|
||||
{
|
||||
var result = source;
|
||||
@@ -123,19 +129,16 @@ namespace Sieve.Services
|
||||
|
||||
try
|
||||
{
|
||||
// Filter
|
||||
if (applyFiltering)
|
||||
{
|
||||
result = ApplyFiltering(model, result, dataForCustomMethods);
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (applySorting)
|
||||
{
|
||||
result = ApplySorting(model, result, dataForCustomMethods);
|
||||
}
|
||||
|
||||
// Paginate
|
||||
if (applyPagination)
|
||||
{
|
||||
result = ApplyPagination(model, result);
|
||||
@@ -145,25 +148,21 @@ namespace Sieve.Services
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_options.Value.ThrowExceptions)
|
||||
{
|
||||
if (ex is SieveException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new SieveException(ex.Message, ex);
|
||||
}
|
||||
else
|
||||
if (!_options.Value.ThrowExceptions)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ex is SieveException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new SieveException(ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyFiltering<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result,
|
||||
private IQueryable<TEntity> ApplyFiltering<TEntity>(TSieveModel model, IQueryable<TEntity> result,
|
||||
object[] dataForCustomMethods = null)
|
||||
{
|
||||
if (model?.GetFiltersParsed() == null)
|
||||
@@ -181,25 +180,20 @@ namespace Sieve.Services
|
||||
var (fullPropertyName, property) = GetSieveProperty<TEntity>(false, true, filterTermName);
|
||||
if (property != null)
|
||||
{
|
||||
Expression propertyValue = parameter;
|
||||
Expression nullCheck = null;
|
||||
var names = fullPropertyName.Split('.');
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
if (filterTerm.Values == null)
|
||||
{
|
||||
propertyValue = Expression.PropertyOrField(propertyValue, names[i]);
|
||||
|
||||
if (i != names.Length - 1 && propertyValue.Type.IsNullable())
|
||||
{
|
||||
nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filterTerm.Values == null) continue;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(property.PropertyType);
|
||||
foreach (var filterTermValue in filterTerm.Values)
|
||||
{
|
||||
var isFilterTermValueNull = filterTermValue.ToLower() == nullFilterValue;
|
||||
var (propertyValue, nullCheck) =
|
||||
GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName);
|
||||
|
||||
var isFilterTermValueNull =
|
||||
IsFilterTermValueNull(propertyValue, filterTerm, filterTermValue);
|
||||
|
||||
var filterValue = isFilterTermValueNull
|
||||
? Expression.Constant(null, property.PropertyType)
|
||||
: ConvertStringValueToConstantExpression(filterTermValue, property, converter);
|
||||
@@ -208,11 +202,11 @@ namespace Sieve.Services
|
||||
{
|
||||
propertyValue = Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
|
||||
filterValue = Expression.Call(filterValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
}
|
||||
|
||||
var expression = GetExpression(filterTerm, filterValue, propertyValue);
|
||||
@@ -222,60 +216,97 @@ namespace Sieve.Services
|
||||
expression = Expression.Not(expression);
|
||||
}
|
||||
|
||||
var filterValueNullCheck = !isFilterTermValueNull && propertyValue.Type.IsNullable()
|
||||
? GenerateFilterNullCheckExpression(propertyValue, nullCheck)
|
||||
: nullCheck;
|
||||
|
||||
var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull);
|
||||
if (filterValueNullCheck != null)
|
||||
{
|
||||
expression = Expression.AndAlso(filterValueNullCheck, expression);
|
||||
}
|
||||
|
||||
if (innerExpression == null)
|
||||
{
|
||||
innerExpression = expression;
|
||||
}
|
||||
else
|
||||
{
|
||||
innerExpression = Expression.OrElse(innerExpression, expression);
|
||||
}
|
||||
innerExpression = innerExpression == null
|
||||
? expression
|
||||
: Expression.OrElse(innerExpression, expression);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ApplyCustomMethod(result, filterTermName, _customFilterMethods,
|
||||
new object[] {
|
||||
result,
|
||||
filterTerm.Operator,
|
||||
filterTerm.Values
|
||||
}, dataForCustomMethods);
|
||||
|
||||
new object[] {result, filterTerm.Operator, filterTerm.Values}, dataForCustomMethods);
|
||||
}
|
||||
}
|
||||
|
||||
if (outerExpression == null)
|
||||
{
|
||||
outerExpression = innerExpression;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerExpression == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
outerExpression = Expression.AndAlso(outerExpression, innerExpression);
|
||||
}
|
||||
|
||||
return outerExpression == null
|
||||
? result
|
||||
: result.Where(Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameter));
|
||||
}
|
||||
|
||||
private static Expression GenerateFilterNullCheckExpression(Expression propertyValue, Expression nullCheckExpression)
|
||||
private static Expression GetFilterValueNullCheck(Expression parameter, string fullPropertyName,
|
||||
bool isFilterTermValueNull)
|
||||
{
|
||||
var (propertyValue, nullCheck) = GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName);
|
||||
|
||||
if (!isFilterTermValueNull && propertyValue.Type.IsNullable())
|
||||
{
|
||||
return GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
|
||||
return nullCheck;
|
||||
}
|
||||
|
||||
private static bool IsFilterTermValueNull(Expression propertyValue, TFilterTerm filterTerm,
|
||||
string filterTermValue)
|
||||
{
|
||||
var isNotString = propertyValue.Type != typeof(string);
|
||||
|
||||
var isValidStringNullOperation = filterTerm.OperatorParsed == FilterOperator.Equals ||
|
||||
filterTerm.OperatorParsed == FilterOperator.NotEquals;
|
||||
|
||||
return filterTermValue.ToLower() == NullFilterValue && (isNotString || isValidStringNullOperation);
|
||||
}
|
||||
|
||||
private static (Expression propertyValue, Expression nullCheck) GetPropertyValueAndNullCheckExpression(
|
||||
Expression parameter, string fullPropertyName)
|
||||
{
|
||||
var propertyValue = parameter;
|
||||
Expression nullCheck = null;
|
||||
var names = fullPropertyName.Split('.');
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
{
|
||||
propertyValue = Expression.PropertyOrField(propertyValue, names[i]);
|
||||
|
||||
if (i != names.Length - 1 && propertyValue.Type.IsNullable())
|
||||
{
|
||||
nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
}
|
||||
|
||||
return (propertyValue, nullCheck);
|
||||
}
|
||||
|
||||
private static Expression GenerateFilterNullCheckExpression(Expression propertyValue,
|
||||
Expression nullCheckExpression)
|
||||
{
|
||||
return nullCheckExpression == null
|
||||
? Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type))
|
||||
: Expression.AndAlso(nullCheckExpression, Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)));
|
||||
: Expression.AndAlso(nullCheckExpression,
|
||||
Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)));
|
||||
}
|
||||
|
||||
private Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, TypeConverter converter)
|
||||
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property,
|
||||
TypeConverter converter)
|
||||
{
|
||||
dynamic constantVal = converter.CanConvertFrom(typeof(string))
|
||||
? converter.ConvertFrom(value)
|
||||
@@ -286,47 +317,34 @@ namespace Sieve.Services
|
||||
|
||||
private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue)
|
||||
{
|
||||
switch (filterTerm.OperatorParsed)
|
||||
return filterTerm.OperatorParsed switch
|
||||
{
|
||||
case FilterOperator.Equals:
|
||||
return Expression.Equal(propertyValue, filterValue);
|
||||
case FilterOperator.NotEquals:
|
||||
return Expression.NotEqual(propertyValue, filterValue);
|
||||
case FilterOperator.GreaterThan:
|
||||
return Expression.GreaterThan(propertyValue, filterValue);
|
||||
case FilterOperator.LessThan:
|
||||
return Expression.LessThan(propertyValue, filterValue);
|
||||
case FilterOperator.GreaterThanOrEqualTo:
|
||||
return Expression.GreaterThanOrEqual(propertyValue, filterValue);
|
||||
case FilterOperator.LessThanOrEqualTo:
|
||||
return Expression.LessThanOrEqual(propertyValue, filterValue);
|
||||
case FilterOperator.Contains:
|
||||
return Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
|
||||
filterValue);
|
||||
case FilterOperator.StartsWith:
|
||||
return Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
|
||||
filterValue);
|
||||
default:
|
||||
return Expression.Equal(propertyValue, filterValue);
|
||||
}
|
||||
FilterOperator.Equals => Expression.Equal(propertyValue, filterValue),
|
||||
FilterOperator.NotEquals => Expression.NotEqual(propertyValue, filterValue),
|
||||
FilterOperator.GreaterThan => Expression.GreaterThan(propertyValue, filterValue),
|
||||
FilterOperator.LessThan => Expression.LessThan(propertyValue, filterValue),
|
||||
FilterOperator.GreaterThanOrEqualTo => Expression.GreaterThanOrEqual(propertyValue, filterValue),
|
||||
FilterOperator.LessThanOrEqualTo => Expression.LessThanOrEqual(propertyValue, filterValue),
|
||||
FilterOperator.Contains => Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
|
||||
filterValue),
|
||||
FilterOperator.StartsWith => Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
|
||||
filterValue),
|
||||
_ => Expression.Equal(propertyValue, filterValue)
|
||||
};
|
||||
}
|
||||
|
||||
// Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core
|
||||
// See https://github.com/aspnet/EntityFrameworkCore/issues/3361
|
||||
// Expression.Constant passed the target type to allow Nullable comparison
|
||||
// See http://bradwilson.typepad.com/blog/2008/07/creating-nullab.html
|
||||
private Expression GetClosureOverConstant<T>(T constant, Type targetType)
|
||||
private static Expression GetClosureOverConstant<T>(T constant, Type targetType)
|
||||
{
|
||||
return Expression.Constant(constant, targetType);
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplySorting<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result,
|
||||
private IQueryable<TEntity> ApplySorting<TEntity>(TSieveModel model, IQueryable<TEntity> result,
|
||||
object[] dataForCustomMethods = null)
|
||||
{
|
||||
if (model?.GetSortsParsed() == null)
|
||||
@@ -346,33 +364,29 @@ namespace Sieve.Services
|
||||
else
|
||||
{
|
||||
result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods,
|
||||
new object[]
|
||||
{
|
||||
result,
|
||||
useThenBy,
|
||||
sortTerm.Descending
|
||||
}, dataForCustomMethods);
|
||||
new object[] {result, useThenBy, sortTerm.Descending}, dataForCustomMethods);
|
||||
}
|
||||
|
||||
useThenBy = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyPagination<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result)
|
||||
private IQueryable<TEntity> ApplyPagination<TEntity>(TSieveModel model, IQueryable<TEntity> result)
|
||||
{
|
||||
var page = model?.Page ?? 1;
|
||||
var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize;
|
||||
var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize;
|
||||
|
||||
if (pageSize > 0)
|
||||
if (pageSize <= 0)
|
||||
{
|
||||
result = result.Skip((page - 1) * pageSize);
|
||||
result = result.Take(Math.Min(pageSize, maxPageSize));
|
||||
return result;
|
||||
}
|
||||
|
||||
result = result.Skip((page - 1) * pageSize);
|
||||
result = result.Take(Math.Min(pageSize, maxPageSize));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -381,51 +395,52 @@ namespace Sieve.Services
|
||||
return mapper;
|
||||
}
|
||||
|
||||
private (string, PropertyInfo) GetSieveProperty<TEntity>(
|
||||
bool canSortRequired,
|
||||
bool canFilterRequired,
|
||||
private (string, PropertyInfo) GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired,
|
||||
string name)
|
||||
{
|
||||
var property = mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
|
||||
if (property.Item1 == null)
|
||||
var property = _mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name,
|
||||
_options.Value.CaseSensitive);
|
||||
if (property.Item1 != null)
|
||||
{
|
||||
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
|
||||
return (prop?.Name, prop);
|
||||
return property;
|
||||
}
|
||||
return property;
|
||||
|
||||
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name,
|
||||
_options.Value.CaseSensitive);
|
||||
return (prop?.Name, prop);
|
||||
}
|
||||
|
||||
private PropertyInfo FindPropertyBySieveAttribute<TEntity>(
|
||||
bool canSortRequired,
|
||||
bool canFilterRequired,
|
||||
string name,
|
||||
bool isCaseSensitive)
|
||||
private static PropertyInfo FindPropertyBySieveAttribute<TEntity>(bool canSortRequired, bool canFilterRequired,
|
||||
string name, bool isCaseSensitive)
|
||||
{
|
||||
return Array.Find(typeof(TEntity).GetProperties(), p =>
|
||||
{
|
||||
return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute
|
||||
&& (!canSortRequired || sieveAttribute.CanSort)
|
||||
&& (!canFilterRequired || sieveAttribute.CanFilter)
|
||||
&& (sieveAttribute.Name ?? p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
return Array.Find(typeof(TEntity).GetProperties(),
|
||||
p => p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute SieveAttribute
|
||||
&& (!canSortRequired || SieveAttribute.CanSort)
|
||||
&& (!canFilterRequired || SieveAttribute.CanFilter)
|
||||
&& (SieveAttribute.Name ?? p.Name).Equals(name,
|
||||
isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> result, string name, object parent, object[] parameters, object[] optionalParameters = null)
|
||||
private IQueryable<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> result, string name, object parent,
|
||||
object[] parameters, object[] optionalParameters = null)
|
||||
{
|
||||
var customMethod = parent?.GetType()
|
||||
.GetMethodExt(name,
|
||||
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<TEntity>));
|
||||
_options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<TEntity>));
|
||||
|
||||
|
||||
if (customMethod == null)
|
||||
{
|
||||
// Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> source, ...)`
|
||||
var genericCustomMethod = parent?.GetType()
|
||||
.GetMethodExt(name,
|
||||
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<>));
|
||||
.GetMethodExt(name,
|
||||
_options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<>));
|
||||
|
||||
if (genericCustomMethod != null &&
|
||||
genericCustomMethod.ReturnType.IsGenericType &&
|
||||
@@ -433,7 +448,8 @@ namespace Sieve.Services
|
||||
{
|
||||
var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0];
|
||||
var constraints = genericBaseType.GetGenericParameterConstraints();
|
||||
if (constraints == null || constraints.Length == 0 || constraints.All((t) => t.IsAssignableFrom(typeof(TEntity))))
|
||||
if (constraints == null || constraints.Length == 0 ||
|
||||
constraints.All((t) => t.IsAssignableFrom(typeof(TEntity))))
|
||||
{
|
||||
customMethod = genericCustomMethod.MakeGenericMethod(typeof(TEntity));
|
||||
}
|
||||
@@ -462,40 +478,34 @@ namespace Sieve.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var incompatibleCustomMethods = parent?
|
||||
.GetType()
|
||||
.GetMethods
|
||||
(
|
||||
_options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public |
|
||||
BindingFlags.Instance
|
||||
)
|
||||
.Where(method => string.Equals(method.Name, name,
|
||||
_options.Value.CaseSensitive
|
||||
? StringComparison.InvariantCulture
|
||||
: StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList()
|
||||
??
|
||||
new List<MethodInfo>();
|
||||
var incompatibleCustomMethods =
|
||||
parent?
|
||||
.GetType()
|
||||
.GetMethods(_options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(method => string.Equals(method.Name, name,
|
||||
_options.Value.CaseSensitive
|
||||
? StringComparison.InvariantCulture
|
||||
: StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList()
|
||||
?? new List<MethodInfo>();
|
||||
|
||||
if (incompatibleCustomMethods.Any())
|
||||
{
|
||||
var incompatibles =
|
||||
from incompatibleCustomMethod in incompatibleCustomMethods
|
||||
let expected = typeof(IQueryable<TEntity>)
|
||||
let actual = incompatibleCustomMethod.ReturnType
|
||||
select new SieveIncompatibleMethodException(name, expected, actual,
|
||||
$"{name} failed. Expected a custom method for type {expected} but only found for type {actual}");
|
||||
|
||||
var aggregate = new AggregateException(incompatibles);
|
||||
|
||||
throw new SieveIncompatibleMethodException(aggregate.Message, aggregate);
|
||||
}
|
||||
else
|
||||
if (!incompatibleCustomMethods.Any())
|
||||
{
|
||||
throw new SieveMethodNotFoundException(name, $"{name} not found.");
|
||||
}
|
||||
|
||||
var incompatibles =
|
||||
from incompatibleCustomMethod in incompatibleCustomMethods
|
||||
let expected = typeof(IQueryable<TEntity>)
|
||||
let actual = incompatibleCustomMethod.ReturnType
|
||||
select new SieveIncompatibleMethodException(name, expected, actual,
|
||||
$"{name} failed. Expected a custom method for type {expected} but only found for type {actual}");
|
||||
|
||||
var aggregate = new AggregateException(incompatibles);
|
||||
|
||||
throw new SieveIncompatibleMethodException(aggregate.Message, aggregate);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@@ -1,22 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Version>2.3.3</Version>
|
||||
<Description>Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/
|
||||
</Description>
|
||||
<Copyright>Copyright 2018</Copyright>
|
||||
<PackageLicenseUrl>https://github.com/Biarity/Sieve/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Description>Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/</Description>
|
||||
<Authors>2018 Biarity, 2021 Kevin Sommer</Authors>
|
||||
|
||||
<PackageTags>Filter;Sort;Page;Paging;</PackageTags>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/Biarity/Sieve</PackageProjectUrl>
|
||||
<PackageIconUrl>https://emojipedia-us.s3.amazonaws.com/thumbs/240/twitter/120/alembic_2697.png</PackageIconUrl>
|
||||
<RepositoryUrl></RepositoryUrl>
|
||||
<PackageReleaseNotes>Only Skip when pageSize > 0 (#63)
|
||||
Added support for generic filter and sort methods (#60)
|
||||
Don't process when filterTerm.Values is null (#59)
|
||||
</PackageReleaseNotes>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<Authors>Biarity</Authors>
|
||||
|
||||
<RepositoryUrl>https://github.com/Biarity/Sieve</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
<!-- Declare that the Repository URL can be published to NuSpec -->
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<!-- Embed source files that are not tracked by the source control manager to the PDB -->
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!-- Include PDB in the built .nupkg -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Reference in New Issue
Block a user