Added case-insensitive operators and started unit tests project

This commit is contained in:
Biarity 2018-02-10 10:26:32 +10:00
parent aa6a836cfb
commit 2796197cc9
13 changed files with 356 additions and 88 deletions

View File

@ -1,11 +1,13 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2010
VisualStudioVersion = 15.0.27130.2027
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveTests", "SieveTests\SieveTests.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SieveUnitTests", "SieveUnitTests\SieveUnitTests.csproj", "{21C3082D-F40E-457F-BE2E-AA099E19E199}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -21,6 +23,10 @@ Global
{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
{21C3082D-F40E-457F-BE2E-AA099E19E199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21C3082D-F40E-457F-BE2E-AA099E19E199}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21C3082D-F40E-457F-BE2E-AA099E19E199}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21C3082D-F40E-457F-BE2E-AA099E19E199}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -13,6 +13,6 @@ namespace Sieve.Models
GreaterThanOrEqualTo,
LessThanOrEqualTo,
Contains,
StartsWith
StartsWith,
}
}

View File

@ -9,6 +9,9 @@ namespace Sieve.Models
{
private string _filter;
private string[] operators = new string[] {
"==*",
"@=*",
"_=*",
"==",
"!=",
">",
@ -16,7 +19,8 @@ namespace Sieve.Models
">=",
"<=",
"@=",
"_=" };
"_="
};
public FilterTerm(string filter)
{
@ -67,6 +71,7 @@ namespace Sieve.Models
switch (Operator.Trim().ToLower())
{
case "==":
case "==*":
return FilterOperator.Equals;
case "!=":
return FilterOperator.NotEquals;
@ -79,8 +84,10 @@ namespace Sieve.Models
case "<=":
return FilterOperator.LessThanOrEqualTo;
case "@=":
case "@=*":
return FilterOperator.Contains;
case "_=":
case "_=*":
return FilterOperator.StartsWith;
default:
return FilterOperator.Equals;

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Text;

View File

@ -49,6 +49,14 @@ namespace Sieve.Services
_options = options;
}
/// <summary>
/// Apply filtering, sorting, and pagination parameters found in `model` to `source`
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="model">An instance of ISieveModel</param>
/// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyAll<TEntity>(ISieveModel model, IQueryable<TEntity> source, object[] dataForCustomMethods = null)
{
var result = source;
@ -67,7 +75,112 @@ namespace Sieve.Services
return result;
}
/// <summary>
/// Apply filtering parameters found in `model` to `source`
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="model">An instance of ISieveModel</param>
/// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyFiltering<TEntity>(ISieveModel model, IQueryable<TEntity> result, object[] dataForCustomMethods = null)
{
if (model?.FiltersParsed == null)
return result;
foreach (var filterTerm in model.FiltersParsed)
{
var property = GetSieveProperty<TEntity>(false, true, filterTerm.Name);
if (property != null)
{
var converter = TypeDescriptor.GetConverter(property.PropertyType);
var parameter = Expression.Parameter(typeof(TEntity), "e");
dynamic filterValue = Expression.Constant(
converter.CanConvertFrom(typeof(string)) ?
converter.ConvertFrom(filterTerm.Value) :
Convert.ChangeType(filterTerm.Value, property.PropertyType));
dynamic propertyValue = Expression.PropertyOrField(parameter, property.Name);
if (filterTerm.Operator.Contains("*"))
{
propertyValue = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
filterValue = Expression.Call(filterValue,
typeof(string).GetMethods()
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
}
Expression comparison;
switch (filterTerm.OperatorParsed)
{
case FilterOperator.Equals:
comparison = Expression.Equal(propertyValue, filterValue);
break;
case FilterOperator.NotEquals:
comparison = Expression.NotEqual(propertyValue, filterValue);
break;
case FilterOperator.GreaterThan:
comparison = Expression.GreaterThan(propertyValue, filterValue);
break;
case FilterOperator.LessThan:
comparison = Expression.LessThan(propertyValue, filterValue);
break;
case FilterOperator.GreaterThanOrEqualTo:
comparison = Expression.GreaterThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.LessThanOrEqualTo:
comparison = Expression.LessThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.Contains:
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
filterValue);
break;
case FilterOperator.StartsWith:
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
filterValue); break;
default:
comparison = Expression.Equal(propertyValue, filterValue);
break;
}
result = result.Where(Expression.Lambda<Func<TEntity, bool>>(
comparison,
parameter));
}
else
{
result = ApplyCustomMethod(result, filterTerm.Name, _customFilterMethods,
new object[] {
result,
filterTerm.Operator,
filterTerm.Value
}, dataForCustomMethods);
}
}
return result;
}
/// <summary>
/// Apply sorting parameters found in `model` to `source`
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="model">An instance of ISieveModel</param>
/// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplySorting<TEntity>(ISieveModel model, IQueryable<TEntity> result, object[] dataForCustomMethods = null)
{
if (model?.SortsParsed == null)
@ -97,84 +210,15 @@ namespace Sieve.Services
return result;
}
public IQueryable<TEntity> ApplyFiltering<TEntity>(ISieveModel model, IQueryable<TEntity> result, object[] dataForCustomMethods = null)
{
if (model?.FiltersParsed == null)
return result;
foreach (var filterTerm in model.FiltersParsed)
{
var property = GetSieveProperty<TEntity>(false, true, filterTerm.Name);
if (property != null)
{
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:
comparison = Expression.Equal(propertyValue, filterValue);
break;
case FilterOperator.NotEquals:
comparison = Expression.NotEqual(propertyValue, filterValue);
break;
case FilterOperator.GreaterThan:
comparison = Expression.GreaterThan(propertyValue, filterValue);
break;
case FilterOperator.LessThan:
comparison = Expression.LessThan(propertyValue, filterValue);
break;
case FilterOperator.GreaterThanOrEqualTo:
comparison = Expression.GreaterThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.LessThanOrEqualTo:
comparison = Expression.LessThanOrEqual(propertyValue, filterValue);
break;
case FilterOperator.Contains:
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
filterValue);
break;
case FilterOperator.StartsWith:
comparison = Expression.Call(propertyValue,
typeof(string).GetMethods()
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
filterValue); break;
default:
comparison = Expression.Equal(propertyValue, filterValue);
break;
}
result = result.Where(Expression.Lambda<Func<TEntity, bool>>(
comparison,
parameter));
}
else
{
result = ApplyCustomMethod(result, filterTerm.Name, _customFilterMethods,
new object[] {
result,
filterTerm.Operator,
filterTerm.Value
}, dataForCustomMethods);
}
}
return result;
}
/// <summary>
/// Apply pagination parameters found in `model` to `source`
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="model">An instance of ISieveModel</param>
/// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyPagination<TEntity>(ISieveModel model, IQueryable<TEntity> result)
{
var page = model?.Page ?? 1;
@ -188,7 +232,7 @@ namespace Sieve.Services
return result;
}
private PropertyInfo GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired, string name)
private PropertyInfo GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired, string name)
{
return typeof(TEntity).GetProperties().FirstOrDefault(p =>
{

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Sieve</id>
<version>1.3.7</version>
<version>1.3.8</version>
<title>Sieve</title>
<authors>Biarity</authors>
<owners>Biarity</owners>
@ -13,7 +13,7 @@
<description>
Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/
</description>
<releaseNotes>Custom operators no longer supported for simplicity</releaseNotes>
<releaseNotes>Filter before sort</releaseNotes>
<copyright>Copyright 2018</copyright>
<tags>aspnetcore filter sort page paginate sieve search</tags>
<dependencies>

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sieve.Attributes;
namespace SieveUnitTests.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;
}
}

97
SieveUnitTests/General.cs Normal file
View File

@ -0,0 +1,97 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities;
using SieveUnitTests.Services;
using System;
using System.Linq;
using System.Collections.Generic;
namespace SieveUnitTests
{
[TestClass]
public class General
{
private SieveProcessor _processor;
private IQueryable<Post> _posts;
public General()
{
_processor = new SieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post>
{
new Post() {
Id = 0,
Title = "A",
LikeCount = 100
},
new Post() {
Id = 1,
Title = "B",
LikeCount = 50
},
new Post() {
Id = 2,
Title = "C",
LikeCount = 0
},
}.AsQueryable();
}
[TestMethod]
public void ContainsCanBeCaseInsensitive()
{
var model = new SieveModel()
{
Filters = "Title@=*a"
};
var result = _processor.ApplyFiltering(model, _posts);
Assert.AreEqual(result.First().Id, 0);
Assert.IsTrue(result.Count() == 1);
}
[TestMethod]
public void ContainsIsCaseSensitive()
{
var model = new SieveModel()
{
Filters = "Title@=a",
};
var result = _processor.ApplyFiltering(model, _posts);
Assert.IsTrue(result.Count() == 0);
}
[TestMethod]
public void EqualsDoesntFailWithNonStringTypes()
{
var model = new SieveModel()
{
Filters = "LikeCount==50",
};
Console.WriteLine(model.FiltersParsed.First().Value);
Console.WriteLine(model.FiltersParsed.First().Operator);
Console.WriteLine(model.FiltersParsed.First().OperatorParsed);
var result = _processor.ApplyFiltering(model, _posts);
Assert.AreEqual(result.First().Id, 1);
Assert.IsTrue(result.Count() == 1);
}
}
}
//
//Sorts = "LikeCount",
//Page = 1,
//PageSize = 10
//

View File

@ -0,0 +1,20 @@
using Sieve.Services;
using SieveUnitTests.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SieveUnitTests.Services
{
public class SieveCustomFilterMethods : ISieveCustomFilterMethods
{
public IQueryable<Post> IsNew(IQueryable<Post> source, string op, string value)
{
var result = source.Where(p => p.LikeCount < 100 &&
p.CommentCount < 5);
return result;
}
}
}

View File

@ -0,0 +1,23 @@
using Sieve.Services;
using SieveUnitTests.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SieveUnitTests.Services
{
public class SieveCustomSortMethods : ISieveCustomSortMethods
{
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc)
{
var result = useThenBy ?
((IOrderedQueryable<Post>)source).ThenBy(p => p.LikeCount) :
source.OrderBy(p => p.LikeCount)
.ThenBy(p => p.CommentCount)
.ThenBy(p => p.DateCreated);
return result;
}
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.Extensions.Options;
using Sieve.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace SieveUnitTests
{
public class SieveOptionsAccessor : IOptions<SieveOptions>
{
private SieveOptions _value;
public SieveOptions Value
{
get
{
return _value;
}
}
public SieveOptionsAccessor()
{
_value = new SieveOptions();
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sieve\Sieve.csproj" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,6 @@
### 1. Pack:
```
dotnet pack Sieve.csproj -c Release -o . /p:PackageVersion=1.3.4
dotnet pack Sieve.csproj -c Release -o . /p:PackageVersion=1.3.8
```
Don't forget to change version since nuget packages are immutable (add one to the nuget current).
@ -13,7 +13,7 @@ Also don't forget updaing `releaseNotes` in nuspec.
### 3. Publish:
```
nuget push Sieve.1.2.0.nupkg API_KEY -Source https://api.nuget.org/v3/index.json
nuget push Sieve.1.3.8.nupkg API_KEY -Source https://api.nuget.org/v3/index.json
```
Replace API_KEY with one you get from nuget's website.
Also don't forget to replace corresponding version.