diff --git a/Sieve.sln b/Sieve.sln
index 16416a5..c6704aa 100644
--- a/Sieve.sln
+++ b/Sieve.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SieveTests", "SieveTests\SieveTests.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8043D264-42A0-4275-97A1-46400C02E37E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8043D264-42A0-4275-97A1-46400C02E37E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8043D264-42A0-4275-97A1-46400C02E37E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8043D264-42A0-4275-97A1-46400C02E37E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Sieve/Attributes/SieveAttribute.cs b/Sieve/Attributes/SieveAttribute.cs
new file mode 100644
index 0000000..019a759
--- /dev/null
+++ b/Sieve/Attributes/SieveAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sieve.Attributes
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ class SieveAttribute : Attribute
+ {
+ ///
+ /// Override name used
+ ///
+ public string Name { get; set; }
+
+ public bool CanSort { get; set; }
+ public bool CanFilter { get; set; }
+ }
+}
diff --git a/Sieve/Class1.cs b/Sieve/Class1.cs
deleted file mode 100644
index 254ddf4..0000000
--- a/Sieve/Class1.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-
-namespace Sieve
-{
- public class Class1
- {
- }
-}
diff --git a/Sieve/Extensions/OrderByWithDirection.cs b/Sieve/Extensions/OrderByWithDirection.cs
new file mode 100644
index 0000000..5521ee7
--- /dev/null
+++ b/Sieve/Extensions/OrderByWithDirection.cs
@@ -0,0 +1,31 @@
+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 OrderByWithDirection
+ (this IEnumerable source,
+ Func keySelector,
+ bool descending)
+ {
+ return descending ? source.OrderByDescending(keySelector)
+ : source.OrderBy(keySelector);
+ }
+
+ public static IOrderedQueryable OrderByWithDirection
+ (this IQueryable source,
+ Expression> keySelector,
+ bool descending)
+ {
+ return descending ? source.OrderByDescending(keySelector)
+ : source.OrderBy(keySelector);
+ }
+
+ }
+}
diff --git a/Sieve/Models/FilterOperator.cs b/Sieve/Models/FilterOperator.cs
new file mode 100644
index 0000000..cbf3675
--- /dev/null
+++ b/Sieve/Models/FilterOperator.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sieve.Models
+{
+ public enum FilterOperator
+ {
+ Equals,
+ GreaterThan,
+ LessThan,
+ GreaterThanOrEqualTo,
+ LessThanOrEqualTo,
+ Contains,
+ StartsWith
+ }
+}
diff --git a/Sieve/Models/FilterTerm.cs b/Sieve/Models/FilterTerm.cs
new file mode 100644
index 0000000..ca28198
--- /dev/null
+++ b/Sieve/Models/FilterTerm.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+
+namespace Sieve.Models
+{
+ public class FilterTerm
+ {
+ public string Name { get; set; }
+
+ public string Operator { get; set; }
+
+ [BindNever]
+ public FilterOperator OperatorParsed {
+ get {
+ switch (Operator.Trim().ToLower())
+ {
+ case "equals":
+ case "eq":
+ case "==":
+ return FilterOperator.Equals;
+ case "lessthan":
+ case "lt":
+ case "<":
+ return FilterOperator.LessThan;
+ case "greaterthan":
+ case "gt":
+ case ">":
+ return FilterOperator.GreaterThan;
+ case "greaterthanorequalto":
+ case "gte":
+ case ">=":
+ return FilterOperator.GreaterThanOrEqualTo;
+ case "lessthanorequalto":
+ case "lte":
+ case "<=":
+ return FilterOperator.LessThanOrEqualTo;
+ case "contains":
+ case "co":
+ case "@=":
+ return FilterOperator.Contains;
+ case "startswith":
+ case "sw":
+ case "_=":
+ return FilterOperator.StartsWith;
+ default:
+ return FilterOperator.Equals;
+ }
+ }
+ }
+
+ public string Value { get; set; }
+
+ public bool Descending { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/Sieve/Models/SieveModel.cs b/Sieve/Models/SieveModel.cs
new file mode 100644
index 0000000..646b08f
--- /dev/null
+++ b/Sieve/Models/SieveModel.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+
+namespace Sieve.Models
+{
+ public class SieveModel
+ {
+ public IEnumerable Filter { get; set; }
+
+ public IEnumerable Sort { get; set; }
+
+ [Range(1, Double.MaxValue)]
+ public int Page { get; set; } = 1;
+
+ [Range(1, Double.MaxValue)]
+ public int PageSize { get; set; } = 10;
+ }
+}
diff --git a/Sieve/Models/SieveOptions.cs b/Sieve/Models/SieveOptions.cs
new file mode 100644
index 0000000..52701ce
--- /dev/null
+++ b/Sieve/Models/SieveOptions.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sieve.Models
+{
+ public class SieveOptions
+ {
+ }
+}
\ No newline at end of file
diff --git a/Sieve/Models/SortTerm.cs b/Sieve/Models/SortTerm.cs
new file mode 100644
index 0000000..86f9b42
--- /dev/null
+++ b/Sieve/Models/SortTerm.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+
+namespace Sieve.Models
+{
+ public class SortTerm
+ {
+ public string Name { get; set; }
+
+ public bool Descending { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/Sieve/Services/ISieveCustomFilterMethods.cs b/Sieve/Services/ISieveCustomFilterMethods.cs
new file mode 100644
index 0000000..067aa61
--- /dev/null
+++ b/Sieve/Services/ISieveCustomFilterMethods.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sieve.Services
+{
+ public interface ISieveCustomFilterMethods
+ {
+ }
+
+ public interface ISieveCustomFilterMethods
+ where TEntity : class
+ {
+ }
+}
diff --git a/Sieve/Services/ISieveCustomSortMethods.cs b/Sieve/Services/ISieveCustomSortMethods.cs
new file mode 100644
index 0000000..e10cdce
--- /dev/null
+++ b/Sieve/Services/ISieveCustomSortMethods.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sieve.Services
+{
+ public interface ISieveCustomSortMethods
+ {
+ }
+
+ public interface ISieveCustomSortMethods
+ where TEntity : class
+ {
+ }
+}
diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs
new file mode 100644
index 0000000..fe05640
--- /dev/null
+++ b/Sieve/Services/SieveProcessor.cs
@@ -0,0 +1,151 @@
+using Microsoft.Extensions.Options;
+using Sieve.Models;
+using System;
+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;
+
+namespace Sieve.Services
+{
+ public class SieveProcessor
+ where TEntity: class
+ {
+ private IOptions _options;
+ private ISieveCustomSortMethods _customSortMethods;
+ private ISieveCustomFilterMethods _customFilterMethods;
+
+ public SieveProcessor(IOptions options,
+ ISieveCustomSortMethods customSortMethods,
+ ISieveCustomFilterMethods customFilterMethods)
+ {
+ _options = options;
+ _customSortMethods = customSortMethods;
+ _customFilterMethods = customFilterMethods;
+ }
+
+ public IEnumerable ApplyAll(SieveModel model, IQueryable source)
+ {
+ var result = source.AsNoTracking();
+
+ // Sort
+ result = ApplySort(model, result);
+
+ // Filter
+ result = ApplyFilter(model, result);
+
+ // Paginate
+ result = ApplyPagination(model, result);
+
+ return result;
+ }
+
+ public IQueryable ApplySort(SieveModel model, IQueryable result)
+ {
+ foreach (var sortTerm in model.Sort)
+ {
+ var property = GetSieveProperty(true, false, sortTerm.Name);
+
+ if (property != null)
+ {
+ result = result.OrderByWithDirection(
+ e => property.GetValue(e),
+ sortTerm.Descending);
+ }
+ else
+ {
+ var customMethod = _customSortMethods.GetType()
+ .GetMethod(sortTerm.Name);
+
+ if (customMethod != null)
+ {
+ result = result.OrderByWithDirection(
+ e => customMethod.Invoke(_customSortMethods, new object[] { e }),
+ sortTerm.Descending);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public IQueryable ApplyFilter(SieveModel model, IQueryable result)
+ {
+ foreach (var filterTerm in model.Filter)
+ {
+ var property = GetSieveProperty(false, true, filterTerm.Name);
+
+ if (property != null)
+ {
+ var filterValue = Convert.ChangeType(filterTerm.Value, property.GetType());
+
+ switch (filterTerm.OperatorParsed)
+ {
+ case FilterOperator.Equals:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).Equals(filterValue));
+ break;
+ case FilterOperator.GreaterThan:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) > 0);
+ break;
+ case FilterOperator.LessThan:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) < 0);
+ break;
+ case FilterOperator.GreaterThanOrEqualTo:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) >= 0);
+ break;
+ case FilterOperator.LessThanOrEqualTo:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).CompareTo(filterValue) <= 0);
+ break;
+ case FilterOperator.Contains:
+ result = result.Where(e => ((string)property.GetValue(e)).Contains((string)filterValue));
+ break;
+ case FilterOperator.StartsWith:
+ result = result.Where(e => ((string)property.GetValue(e)).StartsWith((string)filterValue));
+ break;
+ default:
+ result = result.Where(e => ((IComparable)property.GetValue(e)).Equals(filterValue));
+ break;
+ }
+ }
+ else
+ {
+ var customMethod = _customFilterMethods.GetType()
+ .GetMethod(filterTerm.Name);
+
+ if (customMethod != null)
+ {
+ result = result.Where(
+ e => (bool)customMethod.Invoke(_customFilterMethods, new object[] { e }));
+ }
+
+ }
+ }
+
+ return result;
+ }
+
+ public IQueryable ApplyPagination(SieveModel model, IQueryable result)
+ {
+ result = result.Skip((model.Page - 1) * model.PageSize)
+ .Take(model.PageSize);
+ return result;
+ }
+
+ private PropertyInfo GetSieveProperty(bool canSortRequired, bool canFilterRequired, string name)
+ {
+ return typeof(TEntity).GetProperties().FirstOrDefault(p =>
+ {
+ if (p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute)
+ if ((canSortRequired ? sieveAttribute.CanSort : true) &&
+ (canFilterRequired ? sieveAttribute.CanFilter : true) &&
+ ((sieveAttribute.Name ?? p.Name) == name))
+ return true;
+ return false;
+ });
+ }
+ }
+}
diff --git a/Sieve/Sieve.csproj b/Sieve/Sieve.csproj
index 5766db6..a9d176b 100644
--- a/Sieve/Sieve.csproj
+++ b/Sieve/Sieve.csproj
@@ -4,4 +4,10 @@
netcoreapp2.0
+
+
+
+
+
+
diff --git a/SieveTests/Controllers/ValuesController.cs b/SieveTests/Controllers/ValuesController.cs
new file mode 100644
index 0000000..9beb94b
--- /dev/null
+++ b/SieveTests/Controllers/ValuesController.cs
@@ -0,0 +1,44 @@
+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 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)
+ {
+ }
+ }
+}
diff --git a/SieveTests/Program.cs b/SieveTests/Program.cs
new file mode 100644
index 0000000..7d2a893
--- /dev/null
+++ b/SieveTests/Program.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace SieveTests
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BuildWebHost(args).Run();
+ }
+
+ public static IWebHost BuildWebHost(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
+ .UseStartup()
+ .Build();
+ }
+}
diff --git a/SieveTests/Properties/launchSettings.json b/SieveTests/Properties/launchSettings.json
new file mode 100644
index 0000000..a748c44
--- /dev/null
+++ b/SieveTests/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:65136/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "api/values",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "SieveTests": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api/values",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:65137/"
+ }
+ }
+}
diff --git a/SieveTests/SieveTests.csproj b/SieveTests/SieveTests.csproj
new file mode 100644
index 0000000..2a6a149
--- /dev/null
+++ b/SieveTests/SieveTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SieveTests/Startup.cs b/SieveTests/Startup.cs
new file mode 100644
index 0000000..b0bccb6
--- /dev/null
+++ b/SieveTests/Startup.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace SieveTests
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddMvc();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseMvc();
+ }
+ }
+}
diff --git a/SieveTests/appsettings.Development.json b/SieveTests/appsettings.Development.json
new file mode 100644
index 0000000..fa8ce71
--- /dev/null
+++ b/SieveTests/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "IncludeScopes": false,
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/SieveTests/appsettings.json b/SieveTests/appsettings.json
new file mode 100644
index 0000000..26bb0ac
--- /dev/null
+++ b/SieveTests/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "IncludeScopes": false,
+ "Debug": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "Console": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ }
+ }
+}