using System; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Options; using Sieve.Attributes; using Sieve.Exceptions; using Sieve.Extensions; using Sieve.Models; namespace Sieve.Services { public class SieveProcessor : SieveProcessor<SieveModel, FilterTerm, SortTerm>, ISieveProcessor { public SieveProcessor(IOptions<SieveOptions> options) : base(options) { } 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, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) { } } 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, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods) { } public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods) { } public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) { } } public class SieveProcessor<TSieveModel, TFilterTerm, TSortTerm> : ISieveProcessor<TSieveModel, TFilterTerm, TSortTerm> where TSieveModel : class, ISieveModel<TFilterTerm, TSortTerm> where TFilterTerm : IFilterTerm, new() where TSortTerm : ISortTerm, new() { private readonly IOptions<SieveOptions> _options; private readonly ISieveCustomSortMethods _customSortMethods; private readonly ISieveCustomFilterMethods _customFilterMethods; private readonly SievePropertyMapper mapper = new SievePropertyMapper(); public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) { mapper = MapProperties(mapper); _options = options; _customSortMethods = customSortMethods; _customFilterMethods = customFilterMethods; } public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) { mapper = MapProperties(mapper); _options = options; _customSortMethods = customSortMethods; } public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) { mapper = MapProperties(mapper); _options = options; _customFilterMethods = customFilterMethods; } public SieveProcessor(IOptions<SieveOptions> options) { mapper = MapProperties(mapper); _options = options; } /// <summary> /// Apply filtering, sorting, and pagination parameters found in `model` to `source` /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="model">An instance of ISieveModel</param> /// <param name="source">Data source</param> /// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param> /// <param name="applyFiltering">Should the data be filtered? Defaults to true.</param> /// <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, bool applyPagination = true) { var result = source; if (model == null) { return result; } try { // Filter if (applyFiltering) { result = ApplyFiltering(model, result, dataForCustomMethods); } // Sort if (applySorting) { result = ApplySorting(model, result, dataForCustomMethods); } // Paginate if (applyPagination) { result = ApplyPagination(model, result); } return result; } catch (Exception ex) { if (_options.Value.ThrowExceptions) { if (ex is SieveException) { throw; } throw new SieveException(ex.Message, ex); } else { return result; } } } public string GetExtendFilterString<TEntity>( TSieveModel model, bool bCaseSensitive = false) { string fullQueryString = ""; //string dynamicQuery = ""; if (model?.GetFiltersParsed() == null) { return " id != null"; } //Expression outerExpression = null; //var parameterExpression = Expression.Parameter(typeof(TEntity), "e"); foreach (var filterTerm in model.GetFiltersParsed()) { //Expression innerExpression = null; foreach (var filterTermName in filterTerm.Names) { string searchPart = ""; string searchProperty = ""; ////////----------------- var (fullName, property) = GetSieveProperty<TEntity>(false, true, filterTermName); if (property != null) { var converter = TypeDescriptor.GetConverter(property.PropertyType); //dynamic propertyValue = parameterExpression; foreach (object attrib in property.GetCustomAttributes(true)) { searchProperty = attrib.GetType().GetProperty("StringValue").GetValue(attrib, null).ToString(); } //foreach (var part in fullName.Split('.')) //{ // propertyValue = Expression.PropertyOrField(propertyValue, part); //} if (filterTerm.Values == null) continue; foreach (var filterTermValue in filterTerm.Values) { string dynamicQuery = ""; //////---------------- dynamic constantVal = converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(filterTermValue) : Convert.ChangeType(filterTermValue, property.PropertyType); Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType); if (!string.IsNullOrEmpty(searchProperty)) { dynamicQuery = GetDynamicQueryString(filterTerm, searchProperty.ToString(), filterValue.ToString()); if (string.IsNullOrEmpty(fullQueryString)) { searchPart = "(" + dynamicQuery + ")"; } else { searchPart += " Or " + "(" + dynamicQuery + ")"; } } #region advance //if (filterTerm.OperatorIsCaseInsensitive) //{ // propertyValue = Expression.Call(propertyValue, // typeof(string).GetMethods() // .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)); //} //var expression = GetExpression(filterTerm, filterValue, propertyValue, bCaseSensitive); //if (filterTerm.OperatorIsNegated) //{ // expression = Expression.Not(expression); //} //if (innerExpression == null) //{ // innerExpression = expression; //} //else //{ // innerExpression = Expression.Or(innerExpression, expression); //} #endregion } } if (!string.IsNullOrEmpty(searchPart)) { if (string.IsNullOrEmpty(fullQueryString)) { fullQueryString = "(" + searchPart + ")"; } else { fullQueryString += " And " + "(" + searchPart + ")"; } } } } if (string.IsNullOrEmpty(fullQueryString)) { fullQueryString = " id != null"; } return fullQueryString; } protected object ChangeType(object value, Type conversion) { var t = conversion; if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) { if (value == null) { return null; } t = Nullable.GetUnderlyingType(t); } return Convert.ChangeType(value, t); } public Expression<Func<TEntity, bool>> GetFilterExpressionQuery<TEntity>( TSieveModel model, bool bCaseSensitive = false) { Expression outerExpression = null; var parameterExpression = Expression.Parameter(typeof(TEntity), "e"); //string dynamicQuery = ""; if (model?.GetFiltersParsed() == null) { var property = Expression.Property(parameterExpression, "id"); ConstantExpression constant = Expression.Constant(new Guid("{00000000-0000-0000-0000-000000000000}"), typeof(Guid)); var rst = Expression.NotEqual(property, constant); return Expression.Lambda<Func<TEntity, bool>>(rst, parameterExpression); } foreach (var filterTerm in model.GetFiltersParsed()) { Expression innerExpression = null; foreach (var filterTermName in filterTerm.Names) { var (fullName, property) = GetSieveProperty<TEntity>(false, true, filterTermName); if (property != null) { var converter = TypeDescriptor.GetConverter(property.PropertyType); dynamic propertyValue = parameterExpression; foreach (var part in fullName.Split('.')) { propertyValue = Expression.PropertyOrField(propertyValue, part); } if (filterTerm.Values == null) continue; foreach (var filterTermValue in filterTerm.Values) { dynamic constantVal = converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(filterTermValue) : Convert.ChangeType(filterTermValue, property.PropertyType); Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType); if (filterTerm.OperatorIsCaseInsensitive) { propertyValue = Expression.Call(propertyValue, typeof(string).GetMethods() .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)); } var expression = GetExpression(filterTerm, filterValue, propertyValue, bCaseSensitive); if (filterTerm.OperatorIsNegated) { expression = Expression.Not(expression); } if (innerExpression == null) { innerExpression = expression; } else { innerExpression = Expression.Or(innerExpression, expression); } } } } if (outerExpression == null) { outerExpression = innerExpression; continue; } if (innerExpression == null) { continue; } outerExpression = Expression.And(outerExpression, innerExpression); } if (outerExpression == null) { var property = Expression.Property(parameterExpression, "id"); ConstantExpression constant = Expression.Constant(new Guid("{00000000-0000-0000-0000-000000000000}"), typeof(Guid)); var rst = Expression.NotEqual(property, constant); return Expression.Lambda<Func<TEntity, bool>>(rst, parameterExpression); } return Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameterExpression); } private IQueryable<TEntity> ApplyFiltering<TEntity>( TSieveModel model, IQueryable<TEntity> result, object[] dataForCustomMethods = null, bool bCaseSensitive = false) { if (model?.GetFiltersParsed() == null) { return result; } Expression outerExpression = null; var parameterExpression = Expression.Parameter(typeof(TEntity), "e"); foreach (var filterTerm in model.GetFiltersParsed()) { Expression innerExpression = null; foreach (var filterTermName in filterTerm.Names) { var (fullName, property) = GetSieveProperty<TEntity>(false, true, filterTermName); if (property != null) { var converter = TypeDescriptor.GetConverter(property.PropertyType); dynamic propertyValue = parameterExpression; foreach (var part in fullName.Split('.')) { propertyValue = Expression.PropertyOrField(propertyValue, part); } if (filterTerm.Values == null) continue; foreach (var filterTermValue in filterTerm.Values) { dynamic constantVal = converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(filterTermValue) : Convert.ChangeType(filterTermValue, property.PropertyType); Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType); if (filterTerm.OperatorIsCaseInsensitive) { propertyValue = Expression.Call(propertyValue, typeof(string).GetMethods() .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)); } var expression = GetExpression(filterTerm, filterValue, propertyValue, bCaseSensitive); if (filterTerm.OperatorIsNegated) { expression = Expression.Not(expression); } if (innerExpression == null) { innerExpression = expression; } else { innerExpression = Expression.Or(innerExpression, expression); } } } else { result = ApplyCustomMethod(result, filterTermName, _customFilterMethods, new object[] { result, filterTerm.Operator, filterTerm.Values }, dataForCustomMethods); } } if (outerExpression == null) { outerExpression = innerExpression; continue; } if (innerExpression == null) { continue; } outerExpression = Expression.And(outerExpression, innerExpression); } return outerExpression == null ? result : result.Where(Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameterExpression)); } private static string GetDynamicQueryString(TFilterTerm filterTerm, string property, string value) { string sResult = property; switch (filterTerm.OperatorParsed) { case FilterOperator.Equals: return sResult + "=" + value; case FilterOperator.NotEquals: return sResult + "!=" + value; case FilterOperator.GreaterThan: return sResult + ">" + value; case FilterOperator.LessThan: return sResult + "<" + value; case FilterOperator.GreaterThanOrEqualTo: return sResult + ">=" + value; case FilterOperator.LessThanOrEqualTo: return sResult + "<=" + value; case FilterOperator.Contains: return sResult + ".Contains(" + value + ")"; case FilterOperator.StartsWith: return sResult + ".Contains(" + value + ")"; default: return sResult + "=" + value; } } private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue, bool bCaseSensitive) { switch (filterTerm.OperatorParsed) { 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); } } // 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) { return Expression.Constant(constant, targetType); } private IQueryable<TEntity> ApplySorting<TEntity>( TSieveModel model, IQueryable<TEntity> result, object[] dataForCustomMethods = null) { if (model?.GetSortsParsed() == null) { return result; } var useThenBy = false; foreach (var sortTerm in model.GetSortsParsed()) { var (fullName, property) = GetSieveProperty<TEntity>(true, false, sortTerm.Name); if (property != null) { result = result.OrderByDynamic(fullName, property, sortTerm.Descending, useThenBy); } else { result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods, new object[] { result, useThenBy, sortTerm.Descending }, dataForCustomMethods); } useThenBy = true; } return result; } public int ResultCountBeForeApplyPagination = 0; public void GetSkipAndTake( TSieveModel model, ref int skip, ref int take) { var page = model?.Page ?? 1; var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize; skip = (page - 1) * pageSize; take = Math.Min(pageSize, maxPageSize); } public bool applyPageSize = true; private IQueryable<TEntity> ApplyPagination<TEntity>( TSieveModel model, IQueryable<TEntity> result) { var page = model?.Page ?? 1; model.Page = page; var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; model.PageSize = pageSize; var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize; if (ResultCountBeForeApplyPagination==0) { ResultCountBeForeApplyPagination = result.Count(); } if (pageSize > 0) { if (applyPageSize) { result = result.Skip((page - 1) * pageSize); result = result.Take(Math.Min(pageSize, maxPageSize)); } } return result; } protected virtual SievePropertyMapper MapProperties(SievePropertyMapper mapper) { return mapper; } 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 prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive); return (prop?.Name, prop); } return property; } private PropertyInfo FindPropertyBySieveAttribute<TEntity>( bool canSortRequired, bool canFilterRequired, string name, bool isCaseSensitive) { return Array.Find(typeof(TEntity).GetProperties(), p => { if (p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute) { return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute && (canSortRequired ? sieveAttribute.CanSort : true) && (canFilterRequired ? sieveAttribute.CanFilter : true) && ((sieveAttribute.Name ?? p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); } else { return (p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); } /*Comment by thien vo to apply sort/filter by all property return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute && (canSortRequired ? sieveAttribute.CanSort : true) && (canFilterRequired ? sieveAttribute.CanFilter : true) && ((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) { var customMethod = parent?.GetType() .GetMethodExt(name, _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<>)); 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 { result = customMethod.Invoke(parent, parameters) as IQueryable<TEntity>; } catch (TargetParameterCountException) { if (optionalParameters != null) { result = customMethod.Invoke(parent, parameters.Concat(optionalParameters).ToArray()) as IQueryable<TEntity>; } else { throw; } } } else { var incompatibleCustomMethod = parent?.GetType() .GetMethod(name, _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (incompatibleCustomMethod != null) { var expected = typeof(IQueryable<TEntity>); var actual = incompatibleCustomMethod.ReturnType; throw new SieveIncompatibleMethodException(name, expected, actual, $"{name} failed. Expected a custom method for type {expected} but only found for type {actual}"); } else { throw new SieveMethodNotFoundException(name, $"{name} not found."); } } return result; } } }