Sieve

by Biarity

Biarity /Sieve

⚗️ Clean & extensible Sorting, Filtering, and Pagination for ASP.NET Core

468 Stars 55 Forks Last release: Not found Apache License 2.0 168 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

Sieve

⚗️ 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.

NuGet Release

Get Sieve on nuget

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.

1. Add required services

Inject the

SieveProcessor
service. So in
Startup.cs
add:
C#
services.AddScoped();

2. Tell Sieve which properties you'd like to sort/filter in your models

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). So for our
Post
entity model example: ```C# 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; }

[Sieve(CanFilter = true, CanSort = true, Name = "created")] public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;

There is also the `Name` parameter that you can use to have a different name for use by clients.

Alternatively, you can use Fluent API to do the same. This is especially useful if you don't want to use attributes or have multiple APIs.

3. Get sort/filter/page queries by using the Sieve model in your controllers

In the action that handles returning Posts, use SieveModel to get the sort/filter/page query. Apply it to your data by injecting SieveProcessor into the controller and using its Apply<tentity> method. So for instance:

```C# [HttpGet] public JsonResult GetPosts(SieveModel sieveModel) { var result = _dbContext.Posts.AsNoTracking(); // Makes read-only queries faster result = _sieveProcessor.Apply(sieveModel, result); // Returns result after applying the sort/filter/page query in SieveModel to it return Json(result.ToList()); }

You can also explicitly specify if only filtering, sorting, and/or pagination should be applied via optional arguments.

4. Send a request

Send a request

Add custom sort/filter methods

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.

For instance:

C#
services.AddScoped();
services.AddScoped();
Where
SieveCustomSortMethodsOfPosts
for example is: ```C# public class SieveCustomSortMethods : ISieveCustomSortMethods { public IQueryable Popularity(IQueryable source, bool useThenBy, bool desc) // The method is given an indicator of weather to use ThenBy(), and if the query is descending { var result = useThenBy ? ((IOrderedQueryable)source).ThenBy(p => p.LikeCount) : // ThenBy only works on IOrderedQueryable source.OrderBy(p => p.LikeCount) .ThenBy(p => p.CommentCount) .ThenBy(p => p.DateCreated);
    return result; // Must return modified IQueryable
}

public IQueryable Oldest(IQueryable source, bool useThenBy, bool desc) where T : BaseEntity // Generic functions are allowed too { var result = useThenBy ? ((IOrderedQueryable)source).ThenByDescending(p => p.DateCreated) : source.OrderByDescending(p => p.DateCreated);

return result;

}

}

And `SieveCustomFilterMethods`:
C# public class SieveCustomFilterMethods : ISieveCustomFilterMethods { public IQueryable IsNew(IQueryable source, string op, string[] values) // The method is given the {Operator} & {Value} { var result = source.Where(p => p.LikeCount < 100 && p.CommentCount < 5);
    return result; // Must return modified IQueryable
}

public IQueryable Latest(IQueryable 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; }

} ```

Configure Sieve

Use the ASP.NET Core options pattern with

SieveOptions
to tell Sieve where to look for configuration. For example:
C#
services.Configure(Configuration.GetSection("Sieve"));
Then you can add the configuration:
json
{
    "Sieve": {
        "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"
    }
}

Send a request

With all the above in place, you can now send a GET request that includes a sort/filter/page query. An example: ```curl GET /GetPosts

?sorts= LikeCount,CommentCount,-created // sort by likes, then comments, then descendingly by date created &filters= LikeCount>10, [email protected]=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
        * 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`
    * `{Operator}` is one of the [Operators](#operators)
    * `{Value}` is the value to use for filtering
        * You can also have multiple values (for OR logic) by using a pipe delimiter, eg. `[email protected]=new|hot` will return posts with titles that contain the text "`new`" or "`hot`"
* `page` is the number of page to return
* `pageSize` is the number of items returned per page 

Notes:

  • You can use backslashes to escape commas and pipes within value fields
  • 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)
  • Here's a good example on how to work with enumerables
  • Another example on how to do OR logic

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 { public User Creator { get; set; } }

public class User { public string Name { get; set; } }

Mark

Post.User
to be filterable:
C#
// in MapProperties
mapper.Property(p => p.Creator.Name)
    .CanFilter();

Now you can make requests such as:

filters=User.Name==specific_name
.

Creating your own DSL

You can replace this DSL with your own (eg. use JSON instead) by implementing an ISieveModel. You can use the default SieveModel for reference.

Operators

| Operator | Meaning | |------------|--------------------------| |

==
| Equals | |
!=
| Not equals | |
>
| Greater than | |
<
| Less than | |
>=
| Greater than or equal to | |
<=
| Less than or equal to | |
@=
| Contains | |
_=
| Starts with | |
[email protected]=
| Does not Contains | |
!_=
| Does not Starts with | |
@=*
| Case-insensitive string Contains | |
_=*
| Case-insensitive string Starts with | |
==*
| Case-insensitive string Equals | |
!=*
| Case-insensitive string Not equals | |
[email protected]=*
| Case-insensitive string does not Contains | |
!_=*
| Case-insensitive string does not Starts with |

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.

Example project

You can find an example project incorporating most Sieve concepts in SieveTests.

Fluent API

To use the Fluent API instead of attributes in marking properties, setup an alternative

SieveProcessor
that overrides
MapProperties
. For example:
public class ApplicationSieveProcessor : SieveProcessor
{
    public ApplicationSieveProcessor(
        IOptions options, 
        ISieveCustomSortMethods customSortMethods, 
        ISieveCustomFilterMethods customFilterMethods) 
        : base(options, customSortMethods, customFilterMethods)
    {
    }

protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
    mapper.Property<post>(p =&gt; p.Title)
        .CanFilter()
        .HasName("a_different_query_name_here");

    mapper.Property<post>(p =&gt; p.CommentCount)
        .CanSort();

    mapper.Property<post>(p =&gt; p.DateCreated)
        .CanSort()
        .CanFilter()
        .HasName("created_on");

    return mapper;
}

}

Now you should inject the new class instead:

C#
services.AddScoped();

Find More on Sieve's Fluent API here.

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

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

License & Contributing

Sieve is licensed under Apache 2.0. Any contributions highly appreciated!

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.