Strainer is a simple, clean, and extensible framework based on .NET Standard that enables sorting, filtering, and pagination functionality. Documentation available on GitLab: https://gitlab.com/fluorite/strainer/
Install-Package Fluorite.Strainer -Version 3.0.0-preview4
Strainer is a simple, clean and extensible framework based on .NET Standard that makes sorting, filtering and pagination trival.
Note: This project is a port of Sieve with its original author Biarity.
Strainer is divided into following NuGet packages:
Name | Framework | Version |
---|---|---|
Strainer | .NET Standard 2.0 | |
Strainer.AspNetCore | .NET Standard 2.0 |
Using .NET Core CLI:
dotnet add package Fluorite.Strainer.AspNetCore
Using NuGet Package Manager Console:
Install-Package Fluorite.Strainer.AspNetCore
Add Strainer services in Startup
while specifying the implementation of IStrainerProcessor
. For starters, you can use the default implementation - StrainerProcessor
.
services.AddStrainer<StrainerProcessor>();
While adding Strainer, you can configure it with available options.
Strainer will filter/sort by properties that have applied [StrainerProperty]
attribute on them.
In order to mark a property as filterable and sortable, simply apply the [StrainerProperty]
attribute:
[StrainerProperty]
public int Id { get; set; }
Set a custom display name:
[StrainerProperty(DisplayName = "identifier")]
public int Id { get; set; }
Mark property as sortable, but not filterable:
[StrainerProperty(IsFilterable = false)]
public int Id { get; set; }
You can also use [StrainerObject]
attribute to set default values on object level (note that you have to provide name for default sorting property):
[StrainerObject(nameof(Id))]
public class Post
{
public int Id { get; set; }
public DateTime Created { get; set; }
public string Title { get; set; }
}
Alternatively, you can use Fluent API to do the same. This is especially useful if you don't want to/can't use attributes or have multiple APIs.
In example below, Strainer processor is injected to a controller. In GetPosts()
method below, Apply()
is called causing the source collection to be processed. Strainer processor will filter, sort and/or paginate the source IQueryable
depending on model parameters.
private readonly ApplicationDbContext _dbContext;
private readonly IStrainerProcessor _strainerProcessor;
public PostsController(IStrainerProcessor strainerProcessor, ApplicationDbContext dbContext)
{
_dbContext = dbContext;
_strainerProcessor = strainerProcessor;
}
[HttpGet]
public JsonResult GetPosts(StrainerModel strainerModel)
{
var result = _strainerProcessor.Apply(strainerModel, _dbContext.Posts);
return Json(result);
}
You can explicitly specify whether only filtering, sorting, and/or pagination should be applied via optional bool
arguments:
var result = _strainerProcessor.Apply(
strainerModel,
source,
applyFiltering: true,
applySorting: true,
applyPagination: false);
or just call only one desired processing method:
var result = _strainerProcessor.ApplyPagination(strainerModel, source);
This is particulary useful when you want to count the resulted collection before pagination:
var result = _strainerProcessor.Apply(strainerModel, questions, applyPagination: false);
Request.HttpContext.Response.Headers.Add("X-Total-Count", result.Count().ToString());
result = _strainerProcessor.ApplyPagination(strainerModel, result);
You can use Fluent API instead of attributes to mark properties, object and even more. Start with implementing your own processor deriving from StrainerProcessor
:
public class ApplicationStrainerProcessor : StrainerProcessor
{
public ApplicationStrainerProcessor(IStrainerContext context) : base(context)
{
}
}
Enable it in Startup
:
services.AddStrainer<ApplicationStrainerProcessor>();
Then override MapProperties()
, for example:
public class ApplicationStrainerProcessor : StrainerProcessor
{
public ApplicationStrainerProcessor(IStrainerContext context) : base(context)
{
}
protected override void MapProperties(IStrainerPropertyMapper mapper)
{
mapper.Property<Post>(p => p.Title)
.IsSortable()
.IsFilterable()
.HasDisplayName("CustomTitleName");
}
}
Fluent API - as opposed to attributes - allows you to:
Strainer comes with following options
:
Name | Type | Default value | Description |
---|---|---|---|
DefaultPageNumber | int |
1 | Default page number. |
DefaultPageSize | int |
10 | Default page size. |
DefaultSortingWay |
SortingWay (Ascending or Descending ) |
Ascending |
An enum value used when applying default sorting. |
IsCaseInsensitiveForNames | bool |
false | A bool value indictating whether Strainer should operatre in case insensitive mode when comparing names. This affects for example the way of comparing filter names with names of properties marked as filterable. |
IsCaseInsensitiveForValues | bool |
false | A bool value indictating whether Strainer should operatre in case insensitive mode when comparing string values. This affects for example the way of comparing filter value with string value of an actual property. |
MaxPageSize | int |
50 | Maximum page number. |
ThrowExceptions | bool |
false | A bool value indictating whether Strainer should throw StrainerExceptions and the like. |
Use the ASP.NET Core options pattern to tell Strainer where to look for configuration. For example:
services.AddStrainer<StrainerProcessor>(Configuration.GetSection("Strainer"));
Then you can add Strainer configuration to appsetting.json
:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Strainer": {
"DefaultPageSize": 20
}
}
Strainer can be also configured via Action
:
services.AddStrainer<StrainerProcessor>(options => options.DefaultPageSize = 20);
Below you can find a sample HTTP GET request that includes a sort/filter/page query:
GET /GetPosts?sorts=LikeCount,CommentCount,-created&filters=LikeCount>10,Title@=awesome title&page=1&pageSize=10
That request will be translated by Strainer to:
Name | Value | Meaning |
---|---|---|
sorts | LikeCount,CommentCount,-created | Sort ascendingly by like count, then ascendingly by comment count and then descendingly by creation date. |
filters | LikeCount>10,Title@=awesome title | Filter to posts with more then 10 likes and with title containing the phrase "awesome title". |
page | 1 | Select the first page of resulted collection. |
pageSize | 10 | Split the resulted collection into a 10-element pages. |
Strainer model is based on four properties:
Sorts
is a comma-delimited list of property names to sort by. Order of properties does matter. Strainer by default sorts ascendingly (you can configure it via options). Adding a dash prefix (-
) before the property name switches the sorting way to descending. You can control this behaviour with custom sorting way formatter.
Filters
is a comma-delimited list of {Name}{Operator}{Value}
where
{Name}
is the name of a property with the StrainerProperty
attribute or the name of a custom filter method;
(LikeCount|CommentCount)>10
filters to posts where LikeCount
or CommentCount
is greater than 10
;{Operator}
is one of the Operators;{Value}
is the value to use for filtering
Title@=new|hot
filters to posts with titles that contain the phrase "new" or "hot".Page
is the number of page to return.
PageSize
is the number of elements returned per page.
\
) to escape commas and pipes (|
) within value fields to enable conditional filtering;{Name}
or {Operator}
fields;Apply
to defer pagination or explicitly call ApplyPagination()
after manually counting resulted collection (an example);|
).You can replace default StrainerModel
with your own by implementing IStrainerModel
interface. See StrainerModel
for reference.
StrainerModel
comes with no initial validation, so in order to add your own validation rules you should implement your own model or implement a class deriving from StrainerModel
and then override desired properties. For example:
public class ValidatedStrainerModel : StrainerModel
{
[Range(1, 50)]
public override int? PageSize { get; set; }
}
Strainer supports filtering and sorting on nested objects' properties. Mark the property using the Fluent API. Marking via attributes is not currently supported.
For example, using Post
and User
models:
public class Post {
public User Author { get; set; }
}
public class User {
public string Name { get; set; }
}
In order to filter by post author name, override MapProperties
in your custom Strainer processor and provide expression leading to nested property:
protected override void MapProperties(IStrainerPropertyMapper mapper)
{
mapper.Property<Post>(p => p.Author.Name)
.IsFilterable();
}
With such configuration, requests with Filters
set to Author.Name==John_Doe
will tell Strainer to filter to posts with post author name being exactly "John Doe".
Notice how nested property name is not just Name
but it's constructed using full property path resulting in Author.Name
(unless explicitly configured).
In order to add custom sort or filter methods, override appropriate mapping method in your custom Strainer processor.
protected override void MapCustomFilterMethods(ICustomFilterMethodMapper mapper)
{
mapper.CustomMethod<Post>(nameof(IsPopular))
.WithFunction(IsPopular);
}
private IQueryable<Post> IsPopular(ICustomFilterMethodContext<Post> context)
=> context.Source.Where(p => p.LikeCount < 100 && p.CommentCount < 5);
protected override void MapCustomSortMethods(ICustomSortMethodMapper mapper)
{
mapper.CustomMethod<Post>(nameof(Popularity))
.WithFunction(Popularity);
}
private IOrderedQueryable<Post> Popularity(ICustomSortMethodContext<Post> context)
{
return context.IsSubsequent
? context.OrderedSource
.ThenBy(p => p.LikeCount)
.ThenBy(p => p.CommentCount)
.ThenBy(p => p.DateCreated)
: context.Source
.OrderBy(p => p.LikeCount)
.ThenBy(p => p.CommentCount)
.ThenBy(p => p.DateCreated);
}
Notice how conditional ordering is being performed depending on whether context's IsSubsequent
property is true
. That's because Strainer supports subsequent sorting (by multiple properties) with no exception for custom sorting. You can chain them all together.
Strainer comes with following filter operators:
Operator | Meaning |
---|---|
== |
Equals |
!= |
Does not equal |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal to |
<= |
Less than or equal to |
@= |
Contains |
_= |
Starts with |
=_ |
Ends with |
!@= |
Does not contain |
!_= |
Does not start with |
!=_ |
Does not end with |
@=* |
Contains (case-insensitive) |
_=* |
Starts with (case-insensitive) |
=_* |
Ends with (case-insensitive) |
==* |
Equals (case-insensitive) |
!=* |
Does not equal (case-insensitive) |
!@=* |
Does not contain (case-insensitive) |
!_=* |
Does not start with (case-insensitive) |
!=_* |
Does not end with (case-insensitive) |
Case insensitive operators will force case insensitivity when comparing values even when IsCaseInsensitiveForValues
option is set to false
.
Note: even though Strainer supports different case sensitivity modes, whether the case sensitivity will be taken into account when comparing values, depends entirely on the source IQueryable
provider, which in most scenarios is the database provider.
Same manner as marking properties you can add new filter operators. Override MapFilterOperators()
in a class deriving from StrainerProcessor
:
protected override void MapFilterOperators(IFilterOperatorMapper mapper)
{
mapper.AddOperator(symbol: "%")
.HasName("modulo equal zero")
.HasExpression((context) => Expression.Equal(
Expression.Modulo(context.PropertyValue, context.FilterValue),
Expression.Constant(0)));
}
In order to determine sorting way Strainer uses ISortingWayFormatter
. Default implementation used is DescendingPrefixSortingWayFormatter
. It checks against the presence of a prefix indicating descending sorting way, specifically a dash -
. For example:
Name
will be translated to ascending sorting.-Name
will be translated to descending sorting.In order to perform your own sorting way determination and formatting, implement ISortingWayFormatter
interface (see DescendingPrefixSortingWayFormatter for reference).
Then, add custom formatter in Startup
after adding Strainer:
services.AddStrainer<StrainerProcessor>();
services.AddScoped<ISortingWayFormatter, CustomSortingWayFormatter>();
Strainer will silently fail unless ThrowExceptions
in the configuration is set to true
. Following kinds of custom exceptions can be thrown:
StrainerMethodNotFoundException
with a MethodName
StrainerException
which encapsulates any other exception types in its InnerException
It is recommended that you write exception-handling middleware to globally handle Strainer's exceptions when using it with ASP.NET Core.
You can find an example project incorporating most Strainer concepts in Strainer.ExampleWebApi.
A lot happened between Sieve v2* and Strainer v3*. Read the full migration guide here.
Strainer is licensed under Apache 2.0. Any contributions highly appreciated!
Project icon is based on one made by Freepik from www.flaticon.com and Visual Studio Icons.