2018-01-28 01:24:10 +01:00
# Sieve
⚗️ Sieve is a simple, clean, and extensible framework for .NET Core that **adds sorting, filtering, and pagination functionality out of the box** .
2018-01-27 09:09:01 +01:00
Most common use case would be for serving ASP.NET Core GET queries.
2018-01-28 05:16:51 +01:00
[![NuGet Release ](https://img.shields.io/nuget/v/Sieve.svg?style=flat-square )](https://www.nuget.org/packages/Sieve)
2018-01-28 01:08:08 +01:00
[Get Sieve on nuget ](https://www.nuget.org/packages/Sieve/ )
2018-01-28 00:46:59 +01:00
2018-01-27 09:09:01 +01:00
## Usage for ASP.NET Core
In this example, consider an app with a `Post` entity.
We'll use Sieve to add sorting, filtering, and pagination capabilities when GET-ing all available posts.
2018-01-28 00:54:13 +01:00
### 1. Add required services
2018-01-27 09:09:01 +01:00
2018-02-07 05:44:55 +01:00
Inject the `SieveProcessor` service. So in `Startup.cs` add:
2018-06-16 04:18:38 +02:00
```C#
2018-02-07 05:51:34 +01:00
services.AddScoped< SieveProcessor > ();
2018-01-27 09:09:01 +01:00
```
2018-01-28 00:54:13 +01:00
### 2. Tell Sieve which properties you'd like to sort/filter in your models
2018-01-27 09:09:01 +01:00
Sieve will only sort/filter properties that have the attribute `[Sieve(CanSort = true, CanFilter = true)]` on them (they don't have to be both true).
2018-01-27 13:20:00 +01:00
So for our `Post` entity model example:
2018-06-16 04:18:38 +02:00
```C#
2018-01-27 09:09:01 +01:00
public int Id { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public string Title { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public int LikeCount { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public int CommentCount { get; set; }
2018-02-10 02:29:56 +01:00
[Sieve(CanFilter = true, CanSort = true, Name = "created")]
2018-01-27 09:09:01 +01:00
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;
```
2018-02-10 02:29:56 +01:00
There is also the `Name` parameter that you can use to have a different name for use by clients.
2018-01-27 09:09:01 +01:00
2019-01-18 12:04:39 +01:00
Alternatively, you can use [Fluent API ](#fluent-api ) to do the same. This is especially useful if you don't want to use attributes or have multiple APIs.
2018-02-10 07:10:03 +01:00
2018-01-28 00:54:13 +01:00
### 3. Get sort/filter/page queries by using the Sieve model in your controllers
2018-01-27 09:09:01 +01:00
2018-01-28 00:54:13 +01:00
In the action that handles returning Posts, use `SieveModel` to get the sort/filter/page query.
2018-04-20 11:28:53 +02:00
Apply it to your data by injecting `SieveProcessor` into the controller and using its `Apply<TEntity>` method. So for instance:
2018-06-16 04:18:38 +02:00
```C#
2018-01-27 09:09:01 +01:00
[HttpGet]
public JsonResult GetPosts(SieveModel sieveModel)
{
var result = _dbContext.Posts.AsNoTracking(); // Makes read-only queries faster
2018-04-20 11:28:53 +02:00
result = _sieveProcessor.Apply(sieveModel, result); // Returns `result` after applying the sort/filter/page query in `SieveModel` to it
2018-01-27 09:09:01 +01:00
return Json(result.ToList());
}
```
2018-04-20 11:28:53 +02:00
You can also explicitly specify if only filtering, sorting, and/or pagination should be applied via optional arguments.
2018-01-27 09:09:01 +01:00
2018-01-27 12:49:02 +01:00
### 4. Send a request
2018-01-27 09:09:01 +01:00
2018-01-27 12:51:00 +01:00
[Send a request ](#send-a-request )
2018-01-27 09:09:01 +01:00
2018-01-27 12:49:02 +01:00
### Add custom sort/filter methods
2018-01-27 09:09:01 +01:00
2018-02-11 23:58:12 +01:00
If you want to add custom sort/filter methods, inject `ISieveCustomSortMethods` or `ISieveCustomFilterMethods` with the implementation being a class that has custom sort/filter methods that Sieve will search through.
2018-01-27 09:09:01 +01:00
2018-01-27 09:33:48 +01:00
For instance:
2018-06-16 04:18:38 +02:00
```C#
2018-02-07 07:13:47 +01:00
services.AddScoped< ISieveCustomSortMethods , SieveCustomSortMethods > ();
services.AddScoped< ISieveCustomFilterMethods , SieveCustomFilterMethods > ();
2018-01-27 09:33:48 +01:00
```
Where `SieveCustomSortMethodsOfPosts` for example is:
2018-06-16 04:18:38 +02:00
```C#
2018-02-07 05:44:55 +01:00
public class SieveCustomSortMethods : ISieveCustomSortMethods
2018-01-27 09:33:48 +01:00
{
2018-01-27 13:20:00 +01:00
public IQueryable< Post > Popularity(IQueryable< Post > source, bool useThenBy, bool desc) // The method is given an indicator of weather to use ThenBy(), and if the query is descending
2018-01-27 09:33:48 +01:00
{
var result = useThenBy ?
2018-01-28 00:54:13 +01:00
((IOrderedQueryable< Post > )source).ThenBy(p => p.LikeCount) : // ThenBy only works on IOrderedQueryable< TEntity >
2018-01-27 09:33:48 +01:00
source.OrderBy(p => p.LikeCount)
.ThenBy(p => p.CommentCount)
.ThenBy(p => p.DateCreated);
2018-01-27 13:20:00 +01:00
return result; // Must return modified IQueryable< TEntity >
2018-01-27 09:33:48 +01:00
}
2019-03-24 19:45:23 +01:00
public IQueryable< T > Oldest< T > (IQueryable< T > source, bool useThenBy, bool desc) where T : BaseEntity // Generic functions are allowed too
{
var result = useThenBy ?
((IOrderedQueryable< T > )source).ThenByDescending(p => p.DateCreated) :
source.OrderByDescending(p => p.DateCreated);
return result;
}
2018-01-27 09:33:48 +01:00
}
```
2018-02-07 05:44:55 +01:00
And `SieveCustomFilterMethods` :
2018-06-16 04:18:38 +02:00
```C#
2018-02-07 05:44:55 +01:00
public class SieveCustomFilterMethods : ISieveCustomFilterMethods
2018-01-27 09:33:48 +01:00
{
2018-11-30 00:08:39 +01:00
public IQueryable< Post > IsNew(IQueryable< Post > source, string op, string[] values) // The method is given the {Operator} & {Value}
2018-01-27 09:33:48 +01:00
{
var result = source.Where(p => p.LikeCount < 100 & &
p.CommentCount < 5 ) ;
2018-01-27 13:20:00 +01:00
return result; // Must return modified IQueryable< TEntity >
2018-01-27 09:33:48 +01:00
}
2019-03-24 19:45:23 +01:00
public IQueryable< T > Latest< T > (IQueryable< T > source, string op, string[] values) where T : BaseEntity // Generic functions are allowed too
{
var result = source.Where(c => c.DateCreated > DateTimeOffset.UtcNow.AddDays(-14));
return result;
}
2018-01-27 09:33:48 +01:00
}
```
2018-01-27 09:09:01 +01:00
2018-02-13 23:26:12 +01:00
## Configure Sieve
2018-01-27 09:33:48 +01:00
Use the [ASP.NET Core options pattern ](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options ) with `SieveOptions` to tell Sieve where to look for configuration. For example:
2018-06-16 04:18:38 +02:00
```C#
2018-01-27 09:33:48 +01:00
services.Configure< SieveOptions > (Configuration.GetSection("Sieve"));
```
2018-01-27 13:20:00 +01:00
Then you can add the configuration:
2018-06-16 04:18:38 +02:00
```json
2018-01-27 12:49:02 +01:00
{
"Sieve": {
2018-06-16 04:18:38 +02:00
"CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false",
"DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set < =0 to disable paging if no pageSize is specified (default).",
"MaxPageSize": "int number: maximum allowed page size. Set < =0 to make infinite (default)",
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false"
2018-01-27 12:49:02 +01:00
}
}
```
2018-01-27 09:09:01 +01:00
## Send a request
2018-01-28 00:54:13 +01:00
With all the above in place, you can now send a GET request that includes a sort/filter/page query.
2018-01-27 12:49:02 +01:00
An example:
2018-06-16 04:18:38 +02:00
```curl
2018-01-27 09:09:01 +01:00
GET /GetPosts
2018-01-27 12:49:02 +01:00
?sorts= LikeCount,CommentCount,-created // sort by likes, then comments, then descendingly by date created
& filters= LikeCount>10, Title@=awesome title, // filter to posts with more than 10 likes, and a title that contains the phrase "awesome title"
& page= 1 // get the first page...
& pageSize= 10 // ...which contains 10 posts
```
More formally:
* `sorts` is a comma-delimited ordered list of property names to sort by. Adding a `-` before the name switches to sorting descendingly.
* `filters` is a comma-delimited list of `{Name}{Operator}{Value}` where
* `{Name}` is the name of a property with the Sieve attribute or the name of a custom filter method for TEntity
2018-04-20 10:52:13 +02:00
* You can also have multiple names (for OR logic) by enclosing them in brackets and using a pipe delimiter, eg. `(LikeCount|CommentCount)>10` asks if `LikeCount` or `CommentCount` is `>10`
2018-02-08 00:07:38 +01:00
* `{Operator}` is one of the [Operators ](#operators )
2018-01-27 13:21:13 +01:00
* `{Value}` is the value to use for filtering
2018-11-16 09:08:25 +01:00
* You can also have multiple values (for OR logic) by using a pipe delimiter, eg. `Title@=new|hot` will return posts with titles that contain the text "`new`" or "`hot`"
2018-01-27 12:49:02 +01:00
* `page` is the number of page to return
* `pageSize` is the number of items returned per page
Notes:
2018-11-16 09:08:25 +01:00
* You can use backslashes to escape commas and pipes within value fields
2018-01-27 12:49:02 +01:00
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
2018-07-11 03:38:23 +02:00
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example ](https://github.com/Biarity/Sieve/issues/34 ))
2018-02-14 05:12:35 +01:00
* Here's a [good example on how to work with enumerables ](https://github.com/Biarity/Sieve/issues/2 )
* Another example on [how to do OR logic ](https://github.com/Biarity/Sieve/issues/8 )
2018-01-27 12:49:02 +01:00
2019-01-18 11:54:55 +01:00
### Nested objects
You can filter/sort on a nested object's property by marking the property using the Fluent API.
Marking via attributes not currently supported.
For example, using this object model:
```C#
public class Post {
2019-01-18 12:04:39 +01:00
public User Creator { get; set; }
2019-01-18 11:54:55 +01:00
}
public class User {
public string Name { get; set; }
}
```
Mark `Post.User` to be filterable:
```C#
// in MapProperties
2019-01-18 12:04:39 +01:00
mapper.Property< Post > (p => p.Creator.Name)
2019-01-18 11:54:55 +01:00
.CanFilter();
```
2019-01-18 12:04:39 +01:00
Now you can make requests such as: `filters=User.Name==specific_name` .
2019-01-18 11:54:55 +01:00
2018-01-28 01:13:57 +01:00
### Creating your own DSL
2018-01-28 01:14:34 +01:00
You can replace this DSL with your own (eg. use JSON instead) by implementing an [ISieveModel ](https://github.com/Biarity/Sieve/blob/master/Sieve/Models/ISieveModel.cs ). You can use the default [SieveModel ](https://github.com/Biarity/Sieve/blob/master/Sieve/Models/SieveModel.cs ) for reference.
2018-01-27 12:49:02 +01:00
### Operators
| Operator | Meaning |
|------------|--------------------------|
| `==` | Equals |
| `!=` | Not equals |
| `>` | Greater than |
| `<` | Less than |
| `>=` | Greater than or equal to |
| `<=` | Less than or equal to |
| `@=` | Contains |
| `_=` | Starts with |
2019-01-09 11:26:07 +01:00
| `!@=` | Does not Contains |
| `!_=` | Does not Starts with |
2018-02-10 01:25:38 +01:00
| `@=*` | Case-insensitive string Contains |
| `_=*` | Case-insensitive string Starts with |
| `==*` | Case-insensitive string Equals |
2019-11-05 15:10:44 +01:00
| `!=*` | Case-insensitive string Not equals |
2019-01-09 11:26:07 +01:00
| `!@=*` | Case-insensitive string does not Contains |
| `!_=*` | Case-insensitive string does not Starts with |
2018-02-15 10:17:14 +01:00
### Handle Sieve's exceptions
Sieve will silently fail unless `ThrowExceptions` in the configuration is set to true. 3 kinds of custom exceptions can be thrown:
* `SieveMethodNotFoundException` with a `MethodName`
* `SieveIncompatibleMethodException` with a `MethodName` , an `ExpectedType` and an `ActualType`
* `SieveException` which encapsulates any other exception types in its `InnerException`
It is recommended that you write exception-handling middleware to globally handle Sieve's exceptions when using it with ASP.NET Core.
2018-01-28 01:13:57 +01:00
### Example project
You can find an example project incorporating most Sieve concepts in [SieveTests ](https://github.com/Biarity/Sieve/tree/master/SieveTests ).
2019-01-18 12:04:39 +01:00
## Fluent API
To use the Fluent API instead of attributes in marking properties, setup an alternative `SieveProcessor` that overrides `MapProperties` . For example:
2019-01-18 12:05:12 +01:00
```C#
2019-01-18 12:04:39 +01:00
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions< SieveOptions > options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
mapper.Property< Post > (p => p.Title)
.CanFilter()
.HasName("a_different_query_name_here");
mapper.Property< Post > (p => p.CommentCount)
.CanSort();
mapper.Property< Post > (p => p.DateCreated)
.CanSort()
.CanFilter()
.HasName("created_on");
return mapper;
}
}
```
Now you should inject the new class instead:
```C#
services.AddScoped< ISieveProcessor , ApplicationSieveProcessor > ();
```
Find More on Sieve's Fluent API [here ](https://github.com/Biarity/Sieve/issues/4#issuecomment-364629048 ).
2018-11-30 00:08:39 +01:00
## Upgrading to v2.2.0
2.2.0 introduced OR logic for filter values. This means your custom filters will need to accept multiple values rather than just the one.
* In all your custom filter methods, change the last argument to be a `string[] values` instead of `string value`
* The first value can then be found to be `values[0]` rather than `value`
* Multiple values will be present if the client uses OR logic
2018-04-20 11:06:40 +02:00
## Upgrading from v1.* to v2.*
* Changes to the `SieveProcessor` API:
* `ApplyAll` is now `Apply`
* `ApplyFiltering` , `ApplySorting` , and `ApplyPagination` are now depricated - instead you can use optional arguments on `Apply` to achieve the same
* Instead of just removing commas from `{Value}` s, [you'll also need to remove brackets and pipes ](#send-a-request )
2018-01-28 01:13:57 +01:00
## License & Contributing
Sieve is licensed under Apache 2.0. Any contributions highly appreciated!