Base & test solution

This commit is contained in:
Biarity 2018-01-27 09:26:37 +10:00
parent 8407f9e23e
commit 37a6f9f70d
20 changed files with 543 additions and 8 deletions

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SieveTests", "SieveTests\SieveTests.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Sieve.Attributes
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class SieveAttribute : Attribute
{
/// <summary>
/// Override name used
/// </summary>
public string Name { get; set; }
public bool CanSort { get; set; }
public bool CanFilter { get; set; }
}
}

View File

@ -1,8 +0,0 @@
using System;
namespace Sieve
{
public class Class1
{
}
}

View File

@ -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<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

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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<FilterTerm> Filter { get; set; }
public IEnumerable<SortTerm> Sort { get; set; }
[Range(1, Double.MaxValue)]
public int Page { get; set; } = 1;
[Range(1, Double.MaxValue)]
public int PageSize { get; set; } = 10;
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Sieve.Models
{
public class SieveOptions
{
}
}

14
Sieve/Models/SortTerm.cs Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Sieve.Services
{
public interface ISieveCustomFilterMethods
{
}
public interface ISieveCustomFilterMethods<TEntity>
where TEntity : class
{
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Sieve.Services
{
public interface ISieveCustomSortMethods
{
}
public interface ISieveCustomSortMethods<TEntity>
where TEntity : class
{
}
}

View File

@ -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<TEntity>
where TEntity: class
{
private IOptions<SieveOptions> _options;
private ISieveCustomSortMethods<TEntity> _customSortMethods;
private ISieveCustomFilterMethods<TEntity> _customFilterMethods;
public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods<TEntity> customSortMethods,
ISieveCustomFilterMethods<TEntity> customFilterMethods)
{
_options = options;
_customSortMethods = customSortMethods;
_customFilterMethods = customFilterMethods;
}
public IEnumerable<TEntity> ApplyAll(SieveModel model, IQueryable<TEntity> source)
{
var result = source.AsNoTracking();
// Sort
result = ApplySort(model, result);
// Filter
result = ApplyFilter(model, result);
// Paginate
result = ApplyPagination(model, result);
return result;
}
public IQueryable<TEntity> ApplySort(SieveModel model, IQueryable<TEntity> 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<TEntity> ApplyFilter(SieveModel model, IQueryable<TEntity> 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<TEntity> ApplyPagination(SieveModel model, IQueryable<TEntity> 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;
});
}
}
}

View File

@ -4,4 +4,10 @@
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup> </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>
</Project> </Project>

View File

@ -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<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)
{
}
}
}

25
SieveTests/Program.cs Normal file
View File

@ -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<Startup>()
.Build();
}
}

View File

@ -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/"
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
</ItemGroup>
</Project>

40
SieveTests/Startup.cs Normal file
View File

@ -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();
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,15 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}