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>

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Sieve.Models;
using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests.Controllers
{
[Route("api/[controller]/[action]")]
public class TestController : Controller
{
private ISieveProcessor<Post> _sieveProcessor;
private ApplicationDbContext _dbContext;
public TestController(ISieveProcessor<Post> sieveProcessor,
ApplicationDbContext dbContext)
{
_sieveProcessor = sieveProcessor;
_dbContext = dbContext;
}
[HttpGet]
public JsonResult GetAllWithSieve(SieveModel sieveModel)
{
var result = _dbContext.Posts.AsNoTracking();
result = _sieveProcessor.ApplyAll(sieveModel, result);
return Json(result.ToList());
}
[HttpGet]
public JsonResult Create(int number = 10)
{
for (int i = 0; i < number; i++)
{
_dbContext.Posts.Add(new Post());
}
_dbContext.SaveChanges();
return Json(_dbContext.Posts.ToList());
}
[HttpGet]
public JsonResult GetAll()
{
return Json(_dbContext.Posts.ToList());
}
}
}

View File

@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace SieveTests.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

View File

@ -0,0 +1,20 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SieveTests.Entities
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sieve.Attributes;
namespace SieveTests.Entities
{
public class Post
{
public int Id { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 8);
[Sieve(CanFilter = true, CanSort = true)]
public int LikeCount { get; set; } = new Random().Next(0, 1000);
[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;
}
}

View File

@ -0,0 +1,44 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SieveTests.Entities;
using System;
namespace SieveTests.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180127005347_Init")]
partial class Init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("SieveTests.Entities.Post", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CommentCount");
b.Property<DateTimeOffset>("DateCreated");
b.Property<int>("LikeCount");
b.Property<string>("Title");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace SieveTests.Migrations
{
public partial class Init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Posts",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
CommentCount = table.Column<int>(nullable: false),
DateCreated = table.Column<DateTimeOffset>(nullable: false),
LikeCount = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Posts", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Posts");
}
}
}

View File

@ -0,0 +1,43 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SieveTests.Entities;
using System;
namespace SieveTests.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("SieveTests.Entities.Post", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CommentCount");
b.Property<DateTimeOffset>("DateCreated");
b.Property<int>("LikeCount");
b.Property<string>("Title");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -14,7 +14,9 @@ namespace SieveTests
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
var host = BuildWebHost(args);
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>

View File

@ -18,12 +18,11 @@
},
"SieveTests": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:65137/"
"applicationUrl": "http://localhost:6500/"
}
}
}
}

View File

@ -4,6 +4,11 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Migrations\20180127005211_Itit.cs" />
<Compile Remove="Migrations\20180127005211_Itit.Designer.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
@ -16,4 +21,8 @@
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sieve\Sieve.csproj" />
</ItemGroup>
</Project>

View File

@ -4,10 +4,14 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sieve.Models;
using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests
{
@ -24,6 +28,16 @@ namespace SieveTests
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("TestSqlServer")));
services.Configure<SieveOptions>(Configuration.GetSection("SieveConfig"));
//services.AddScoped<ISieveProcessor, SieveProcessor>();
services.AddScoped<ISieveProcessor<Post>, SieveProcessor<Post>>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,4 +1,10 @@
{
"ConnectionStrings": {
"TestSqlServer": "Server=(localdb)\\MSSQLLocalDB; Database=SieveTests;"
},
"SieveConfig": {
},
"Logging": {
"IncludeScopes": false,
"Debug": {