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" + } + } + } +}