Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

Biarity/Sieve

Repository files navigation

⚗️ Sieve is a simple, clean, and extensible framework for .NET Core thatadds sorting, filtering, and pagination functionality out of the box.Most common use case would be for serving ASP.NET Core GET queries.

NuGet ReleaseNuGet Pre-Release

Get Sieve on nuget

Usage for ASP.NET Core

In this example, consider an app with aPost entity.We'll use Sieve to add sorting, filtering, and pagination capabilities when GET-ing all available posts.

1. Add required services

Inject theSieveProcessor service. So inStartup.cs add:

services.AddScoped<SieveProcessor>();

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 ourPost entity model example:

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

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

Alternatively, you can useFluent 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, useSieveModel to get the sort/filter/page query.Apply it to your data by injectingSieveProcessor into the controller and using itsApply<TEntity> method. So for instance:

[HttpGet]publicJsonResultGetPosts(SieveModelsieveModel){varresult=_dbContext.Posts.AsNoTracking();// Makes read-only queries fasterresult=_sieveProcessor.Apply(sieveModel,result);// Returns `result` after applying the sort/filter/page query in `SieveModel` to itreturnJson(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, injectISieveCustomSortMethods orISieveCustomFilterMethods with the implementation being a class that has custom sort/filter methods that Sieve will search through.

For instance:

services.AddScoped<ISieveCustomSortMethods,SieveCustomSortMethods>();services.AddScoped<ISieveCustomFilterMethods,SieveCustomFilterMethods>();

WhereSieveCustomSortMethodsOfPosts for example is:

publicclassSieveCustomSortMethods:ISieveCustomSortMethods{publicIQueryable<Post>Popularity(IQueryable<Post>source,booluseThenBy,booldesc)// The method is given an indicator of whether to use ThenBy(), and if the query is descending{varresult=useThenBy?((IOrderedQueryable<Post>)source).ThenBy(p=>p.LikeCount):// ThenBy only works on IOrderedQueryable<TEntity>source.OrderBy(p=>p.LikeCount).ThenBy(p=>p.CommentCount).ThenBy(p=>p.DateCreated);returnresult;// Must return modified IQueryable<TEntity>}publicIQueryable<T>Oldest<T>(IQueryable<T>source,booluseThenBy,booldesc)whereT:BaseEntity// Generic functions are allowed too{varresult=useThenBy?((IOrderedQueryable<T>)source).ThenByDescending(p=>p.DateCreated):source.OrderByDescending(p=>p.DateCreated);returnresult;}}

AndSieveCustomFilterMethods:

publicclassSieveCustomFilterMethods:ISieveCustomFilterMethods{publicIQueryable<Post>IsNew(IQueryable<Post>source,stringop,string[]values)// The method is given the {Operator} & {Value}{varresult=source.Where(p=>p.LikeCount<100&&p.CommentCount<5);returnresult;// Must return modified IQueryable<TEntity>}publicIQueryable<T>Latest<T>(IQueryable<T>source,stringop,string[]values)whereT:BaseEntity// Generic functions are allowed too{varresult=source.Where(c=>c.DateCreated>DateTimeOffset.UtcNow.AddDays(-14));returnresult;}}

Configure Sieve

Use theASP.NET Core options pattern withSieveOptions to tell Sieve where to look for configuration. For example:

services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));

Then you can add the configuration:

{"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","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"    }}

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:

GET /GetPosts?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
      • You can also have multiple names (for OR logic) by enclosing them in brackets and using a pipe delimiter, eg.(LikeCount|CommentCount)>10 asks ifLikeCount orCommentCount is>10
    • {Operator} is one of theOperators
    • {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"
  • 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)
  • You can have spaces anywhere exceptwithin{Name} or{Operator} fields
  • If you need to look at the data before applying pagination (eg. get total count), use the optional paramters onApply to defer pagination (anexample)
  • Here's agood example on how to work with enumerables
  • Another example onhow 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:

publicclassPost{publicUserCreator{get;set;}}publicclassUser{publicstringName{get;set;}}

MarkPost.User to be filterable:

// in MapPropertiesmapper.Property<Post>(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 anISieveModel. You can use the defaultSieveModel for reference.

Operators

OperatorMeaning
==Equals
!=Not equals
>Greater than
<Less than
>=Greater than or equal to
<=Less than or equal to
@=Contains
_=Starts with
_-=Ends with
!@=Does not Contains
!_=Does not Starts with
!_-=Does not Ends with
@=*Case-insensitive string Contains
_=*Case-insensitive string Starts with
_-=*Case-insensitive string Ends with
==*Case-insensitive string Equals
!=*Case-insensitive string Not equals
!@=*Case-insensitive string does not Contains
!_=*Case-insensitive string does not Starts with

Handle Sieve's exceptions

Sieve will silently fail unlessThrowExceptions in the configuration is set to true. 3 kinds of custom exceptions can be thrown:

  • SieveMethodNotFoundException with aMethodName
  • SieveIncompatibleMethodException with aMethodName, anExpectedType and anActualType
  • SieveException which encapsulates any other exception types in itsInnerException

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 inSieveTests.

Fluent API

To use the Fluent API instead of attributes in marking properties, setup an alternativeSieveProcessor that overridesMapProperties. Forexample:

publicclassApplicationSieveProcessor:SieveProcessor{publicApplicationSieveProcessor(IOptions<SieveOptions>options,ISieveCustomSortMethodscustomSortMethods,ISieveCustomFilterMethodscustomFilterMethods):base(options,customSortMethods,customFilterMethods){}protectedoverrideSievePropertyMapperMapProperties(SievePropertyMappermapper){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");returnmapper;}}

Now you should inject the new class instead:

services.AddScoped<ISieveProcessor,ApplicationSieveProcessor>();

Find More on Sieve's Fluent APIhere.

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 theISieveConfiguration interface was created together with extensions to the default mapper.

publicclassSieveConfigurationForPost:ISieveConfiguration{publicvoidConfigure(SievePropertyMappermapper){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");returnmapper;}}

With the processor simplified to:

publicclassApplicationSieveProcessor:SieveProcessor{publicApplicationSieveProcessor(IOptions<SieveOptions>options,ISieveCustomSortMethodscustomSortMethods,ISieveCustomFilterMethodscustomFilterMethods):base(options,customSortMethods,customFilterMethods){}protectedoverrideSievePropertyMapperMapProperties(SievePropertyMappermapper){returnmapper.ApplyConfiguration<SieveConfigurationForPost>().ApplyConfiguration<SieveConfigurationForComment>();}}

There is also the option to scan and add all configurations for a given assembly

publicclassApplicationSieveProcessor:SieveProcessor{publicApplicationSieveProcessor(IOptions<SieveOptions>options,ISieveCustomSortMethodscustomSortMethods,ISieveCustomFilterMethodscustomFilterMethods):base(options,customSortMethods,customFilterMethods){}protectedoverrideSievePropertyMapperMapProperties(SievePropertyMappermapper){returnmapper.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 astring[] values instead ofstring value
  • The first value can then be found to bevalues[0] rather thanvalue
  • Multiple values will be present if the client uses OR logic

Upgrading from v1.* to v2.*

  • Changes to theSieveProcessor API:
    • ApplyAll is nowApply
    • ApplyFiltering,ApplySorting, andApplyPagination are now depricated - instead you can use optional arguments onApply 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!

About

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

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors24


[8]ページ先頭

©2009-2026 Movatter.jp