Basic working & complete tests

This commit is contained in:
Biarity
2018-01-27 15:20:57 +10:00
parent 37a6f9f70d
commit 0f7ecfb36c
21 changed files with 509 additions and 141 deletions

View File

@@ -5,7 +5,7 @@ using System.Text;
namespace Sieve.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class SieveAttribute : Attribute
public class SieveAttribute : Attribute
{
/// <summary>
/// Override name used

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Sieve.Extensions
{
public static partial class LinqExtentions
{
public static IQueryable<TEntity> OrderByDynamic<TEntity>(this IQueryable<TEntity> source, string orderByProperty,
bool desc, bool useThenBy)
{
string command = desc ?
( useThenBy ? "ThenByDescending" : "OrderByDescending") :
( useThenBy ? "ThenBy" : "OrderBy");
var type = typeof(TEntity);
var property = type.GetProperty(orderByProperty);
var parameter = Expression.Parameter(type, "p");
var propertyAccess = Expression.MakeMemberAccess(parameter, property);
var orderByExpression = Expression.Lambda(propertyAccess, parameter);
var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType },
source.Expression, Expression.Quote(orderByExpression));
return source.Provider.CreateQuery<TEntity>(resultExpression);
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace Sieve.Extensions
{
public static class LinqExtensions
{
public static IOrderedEnumerable<TSource> OrderByWithDirection<TSource, TKey>
(this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
bool descending)
{
return descending ? source.OrderByDescending(keySelector)
: source.OrderBy(keySelector);
}
public static IOrderedQueryable<TSource> OrderByWithDirection<TSource, TKey>
(this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
bool descending)
{
return descending ? source.OrderByDescending(keySelector)
: source.OrderBy(keySelector);
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;
using System.Text;
@@ -8,13 +7,40 @@ namespace Sieve.Models
{
public class FilterTerm
{
public string Name { get; set; }
private string _filter;
public string Operator { get; set; }
public FilterTerm(string filter)
{
_filter = filter;
}
public string Name
{
get
{
return _filter.Split(' ')[0];
}
}
public string Operator
{
get
{
return _filter.Split(' ')[1];
}
}
public string Value {
get
{
return _filter.Split(' ')[2];
}
}
[BindNever]
public FilterOperator OperatorParsed {
get {
get
{
switch (Operator.Trim().ToLower())
{
case "equals":
@@ -51,8 +77,5 @@ namespace Sieve.Models
}
}
public string Value { get; set; }
public bool Descending { get; set; } = false;
}
}

View File

@@ -7,14 +7,55 @@ namespace Sieve.Models
{
public class SieveModel
{
public IEnumerable<FilterTerm> Filter { get; set; }
public string Filters { get; set; }
public IEnumerable<SortTerm> Sort { get; set; }
public string Sorts { get; set; }
[Range(1, Double.MaxValue)]
public int Page { get; set; } = 1;
public int? Page { get; set; }
[Range(1, Double.MaxValue)]
public int PageSize { get; set; } = 10;
public int? PageSize { get; set; }
public List<FilterTerm> FilterParsed
{
get
{
if (Filters != null)
{
var value = new List<FilterTerm>();
foreach (var filter in Filters.Split(','))
{
value.Add(new FilterTerm(filter));
}
return value;
}
else
{
return null;
}
}
}
public List<SortTerm> SortParsed
{
get
{
if (Sorts != null)
{
var value = new List<SortTerm>();
foreach (var sort in Sorts.Split(','))
{
value.Add(new SortTerm(sort));
}
return value;
}
else
{
return null;
}
}
}
}
}

View File

@@ -7,8 +7,41 @@ namespace Sieve.Models
{
public class SortTerm
{
public string Name { get; set; }
private string _sort;
public bool Descending { get; set; } = false;
public SortTerm(string sort)
{
_sort = sort;
}
public string Name
{
get
{
if (_sort.StartsWith('-'))
{
return _sort.Substring(1);
}
else
{
return _sort;
}
}
}
public bool Descending
{
get
{
if (_sort.StartsWith('-'))
{
return true;
}
else
{
return false;
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Sieve.Models;
namespace Sieve.Services
{
//public interface ISieveProcessor : ISieveProcessor<object> { }
public interface ISieveProcessor<TEntity> where TEntity : class
{
IQueryable<TEntity> ApplyAll(SieveModel model, IQueryable<TEntity> source);
IQueryable<TEntity> ApplySorting(SieveModel model, IQueryable<TEntity> result);
IQueryable<TEntity> ApplyFiltering(SieveModel model, IQueryable<TEntity> result);
IQueryable<TEntity> ApplyPagination(SieveModel model, IQueryable<TEntity> result);
}
}

View File

@@ -5,19 +5,34 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;
using System.Reflection;
using Sieve.Attributes;
using Sieve.Extensions;
using System.ComponentModel;
using System.Collections;
using System.Linq.Expressions;
namespace Sieve.Services
{
public class SieveProcessor<TEntity>
//public class SieveProcessor : SieveProcessor<object>, ISieveProcessor
//{
// public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods<object> customSortMethods, ISieveCustomFilterMethods<object> customFilterMethods) : base(options, customSortMethods, customFilterMethods)
// {
// }
//
// public SieveProcessor(IOptions<SieveOptions> options) : base(options)
// {
// }
//
//}
public class SieveProcessor<TEntity> : ISieveProcessor<TEntity>
where TEntity: class
{
private IOptions<SieveOptions> _options;
private ISieveCustomSortMethods<TEntity> _customSortMethods;
private ISieveCustomFilterMethods<TEntity> _customFilterMethods;
public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods<TEntity> customSortMethods,
@@ -28,15 +43,23 @@ namespace Sieve.Services
_customFilterMethods = customFilterMethods;
}
public IEnumerable<TEntity> ApplyAll(SieveModel model, IQueryable<TEntity> source)
public SieveProcessor(IOptions<SieveOptions> options)
{
var result = source.AsNoTracking();
_options = options;
}
public IQueryable<TEntity> ApplyAll(SieveModel model, IQueryable<TEntity> source)
{
var result = source;
if (model == null)
return result;
// Sort
result = ApplySort(model, result);
result = ApplySorting(model, result);
// Filter
result = ApplyFilter(model, result);
result = ApplyFiltering(model, result);
// Paginate
result = ApplyPagination(model, result);
@@ -44,84 +67,95 @@ namespace Sieve.Services
return result;
}
public IQueryable<TEntity> ApplySort(SieveModel model, IQueryable<TEntity> result)
public IQueryable<TEntity> ApplySorting(SieveModel model, IQueryable<TEntity> result)
{
foreach (var sortTerm in model.Sort)
if (model?.SortParsed == null)
return result;
var useThenBy = false;
foreach (var sortTerm in model.SortParsed)
{
var property = GetSieveProperty(true, false, sortTerm.Name);
if (property != null)
{
result = result.OrderByWithDirection(
e => property.GetValue(e),
sortTerm.Descending);
result = result.OrderByDynamic(property.Name, sortTerm.Descending, useThenBy);
}
else
{
var customMethod = _customSortMethods.GetType()
.GetMethod(sortTerm.Name);
if (customMethod != null)
{
result = result.OrderByWithDirection(
e => customMethod.Invoke(_customSortMethods, new object[] { e }),
sortTerm.Descending);
}
result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods,
includeUseThenBy: true,
useThenBy: useThenBy);
}
useThenBy = true;
}
return result;
}
public IQueryable<TEntity> ApplyFilter(SieveModel model, IQueryable<TEntity> result)
public IQueryable<TEntity> ApplyFiltering(SieveModel model, IQueryable<TEntity> result)
{
foreach (var filterTerm in model.Filter)
if (model?.FilterParsed == null)
return result;
foreach (var filterTerm in model.FilterParsed)
{
var property = GetSieveProperty(false, true, filterTerm.Name);
if (property != null)
{
var filterValue = Convert.ChangeType(filterTerm.Value, property.GetType());
var converter = TypeDescriptor.GetConverter(property.PropertyType);
var parameter = Expression.Parameter(typeof(TEntity), "e");
var filterValue = Expression.Constant(
converter.CanConvertFrom(typeof(string)) ?
converter.ConvertFrom(filterTerm.Value) :
Convert.ChangeType(filterTerm.Value, property.PropertyType));
var propertyValue = Expression.PropertyOrField(parameter, property.Name);
Expression comparison;
switch (filterTerm.OperatorParsed)
{
case FilterOperator.Equals:
result = result.Where(e => ((IComparable)property.GetValue(e)).Equals(filterValue));
comparison = Expression.Equal(propertyValue, filterValue);
break;
case FilterOperator.GreaterThan:
result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) > 0);
comparison = Expression.GreaterThan(propertyValue, filterValue);
break;
case FilterOperator.LessThan:
result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) < 0);
comparison = Expression.LessThan(propertyValue, filterValue);
break;
case FilterOperator.GreaterThanOrEqualTo:
result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) >= 0);
comparison = Expression.GreaterThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.LessThanOrEqualTo:
result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) <= 0);
comparison = Expression.LessThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.Contains:
result = result.Where(e => ((string)property.GetValue(e)).Contains((string)filterValue));
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
filterValue);
break;
case FilterOperator.StartsWith:
result = result.Where(e => ((string)property.GetValue(e)).StartsWith((string)filterValue));
break;
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
filterValue); break;
default:
result = result.Where(e => ((IComparable)property.GetValue(e)).Equals(filterValue));
comparison = Expression.Equal(propertyValue, filterValue);
break;
}
result = result.Where(Expression.Lambda<Func<TEntity, bool>>(
comparison,
parameter));
}
else
{
var customMethod = _customFilterMethods.GetType()
.GetMethod(filterTerm.Name);
if (customMethod != null)
{
result = result.Where(
e => (bool)customMethod.Invoke(_customFilterMethods, new object[] { e }));
}
result = ApplyCustomMethod(result, filterTerm.Name, _customFilterMethods);
}
}
@@ -130,8 +164,11 @@ namespace Sieve.Services
public IQueryable<TEntity> ApplyPagination(SieveModel model, IQueryable<TEntity> result)
{
result = result.Skip((model.Page - 1) * model.PageSize)
.Take(model.PageSize);
if (model?.Page == null || model?.PageSize == null)
return result;
result = result.Skip((model.Page.Value - 1) * model.PageSize.Value)
.Take(model.PageSize.Value);
return result;
}
@@ -147,5 +184,21 @@ namespace Sieve.Services
return false;
});
}
private IQueryable<TEntity> ApplyCustomMethod(IQueryable<TEntity> result, string name, object parent,
bool includeUseThenBy = false, bool useThenBy = false)
{
var customMethod = parent?.GetType()
.GetMethod(name);
if (customMethod != null)
{
var parameters = includeUseThenBy ? new object[] { result, useThenBy } : new object[] { result };
result = customMethod.Invoke(parent, parameters)
as IQueryable<TEntity>;
}
return result;
}
}
}

View File

@@ -5,8 +5,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EntityFramework" Version="6.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
</ItemGroup>