Sieve/README.md

362 lines
15 KiB
Markdown
Raw Normal View History

2023-02-23 00:35:18 +01:00
test
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.
[![NuGet Release](https://img.shields.io/nuget/v/Sieve?style=for-the-badge)](https://www.nuget.org/packages/Sieve)
[![NuGet Pre-Release](https://img.shields.io/nuget/vpre/Sieve?style=for-the-badge)](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
{
2021-08-29 16:54:07 +02:00
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc) // The method is given an indicator of whether 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
}
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
{
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
}
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)",
Release Sieve 2.5.0 (#151) * Setup release 2.5.0 with automated build and pre-releases * #80 added support for escaping pipe control characters (#113) * #80 added support for escaping comma and pipe control characters * Update SieveModel.cs Fix build. Accidentally broken by resolving conflicts. * Migrate UnitTests to xUnit Co-authored-by: Clayton Andersen <candersen@restaurant365.com> Co-authored-by: ITDancer13 <kevin@ksommer.eu> Co-authored-by: ITDancer139 <kevinitdancersommer@gmail.com> * SieveProcessor.Options made protected property (#134) Mapper assignment in constructor is moved to a null-coalescing member pair (a field and a property) "IncludeScopes" switch is removed from appSettings.{env}.json files * Revert to _mapper assignment in constructor. (#140) * reverting fix (#142) * Revert to _mapper assignment in constructor. * reverting fix * pass filter values as parameters (#112) make GetClosureOverConstant really work * Make ApplyFiltering, ApplySorting and ApplyPagination protected virtual #139 (#144) * stop excluding null values when filtering using notEqual (#114) * stop excluding null values when filtering using notEqual * add IgnoreNullsOnNotEqual config field, to enable/disable the new feature Co-authored-by: AnasZakarneh <a.zakarneh@foothillsolutions.com> Co-authored-by: Clayton Andersen <tunaman65@gmail.com> Co-authored-by: Clayton Andersen <candersen@restaurant365.com> Co-authored-by: ITDancer139 <kevinitdancersommer@gmail.com> Co-authored-by: Hasan Manzak <hasan.manzak@gmail.com> Co-authored-by: alicak <alicak@users.noreply.github.com> Co-authored-by: AnasZakarneh <Zakarnehanas1@gmail.com> Co-authored-by: AnasZakarneh <a.zakarneh@foothillsolutions.com>
2021-08-29 16:30:19 +02:00
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false",
"IgnoreNullsOnNotEqual": "boolean: ignore null values when filtering using is not equal operator? Defaults to true",
"DisableNullableTypeExpressionForSorting": "boolean: disable the creation of nullable type expression for sorting. Some databases do not handle it (yet). 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
* 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:
* You can use backslashes to escape special characters and sequences:
* commas: `Title@=some\,title` makes a match with "some,title"
* pipes: `Title@=some\|title` makes a match with "some|title"
* null values: `Title@=\null` will search for items with title equal to "null" (not a missing value, but "null"-string literally)
2018-01-27 12:49:02 +01:00
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
* 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 |
| `_-=` | Ends with |
2019-01-09 11:26:07 +01:00
| `!@=` | Does not Contains |
| `!_=` | Does not Starts with |
| `!_-=` | Does not Ends with |
2018-02-10 01:25:38 +01:00
| `@=*` | Case-insensitive string Contains |
| `_=*` | Case-insensitive string Starts with |
| `_-=*` | Case-insensitive string Ends with |
2018-02-10 01:25:38 +01:00
| `==*` | Case-insensitive string Equals |
| `!=*` | 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](https://github.com/Biarity/Sieve/blob/master/Sieve.Sample/Services/ApplicationSieveProcessor.cs):
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;
}
}
```
2019-01-18 12:04:39 +01:00
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).
### Modular Fluent API configuration
Adding all fluent mappings directly in the processor can become unwieldy on larger projects.
It can also clash with vertical architectures.
To enable functional grouping of mappings the `ISieveConfiguration` interface was created together with extensions to the default mapper.
```C#
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(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;
}
}
```
With the processor simplified to:
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper
.ApplyConfiguration<SieveConfigurationForPost>()
.ApplyConfiguration<SieveConfigurationForComment>();
}
}
```
There is also the option to scan and add all configurations for a given assembly
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);
}
}
```
## 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!