Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/plainquire/plainquire
Easy filtering, sorting and paging for ASP.NET Core.
https://github.com/plainquire/plainquire
aps-net-core csharp-library expression-engine filtering linq-expressions openapi paging sorting swagger swagger-ui webapi
Last synced: 2 months ago
JSON representation
Easy filtering, sorting and paging for ASP.NET Core.
- Host: GitHub
- URL: https://github.com/plainquire/plainquire
- Owner: plainquire
- License: mit
- Created: 2021-05-27T08:41:02.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-07-26T05:45:19.000Z (6 months ago)
- Last Synced: 2024-08-18T11:12:22.115Z (6 months ago)
- Topics: aps-net-core, csharp-library, expression-engine, filtering, linq-expressions, openapi, paging, sorting, swagger, swagger-ui, webapi
- Language: C#
- Homepage: https://www.plainquire.com
- Size: 2.17 MB
- Stars: 16
- Watchers: 1
- Forks: 4
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: HISTORY.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Plainquire
[](https://github.com/plainquire/plainquire/releases/latest) [](https://github.com/plainquire/plainquire#MIT-1-ov-file) [](https://github.com/plainquire/plainquire/actions) [](https://github.com/plainquire/plainquire/actions) [](https://plainquire.github.io/plainquire/coveragereport)
Seamless **filtering**, **sorting**, and **paging** for .NET Standard 2.1. Fully **customizable**. Model binding support and integration into Swagger UI.
## Demo
Application: [https://www.plainquire.com/demo](https://www.plainquire.com/demo)
Swagger UI: [https://www.plainquire.com/api](https://www.plainquire.com/api)
## Usage for ASP.NET Core
### 1. Install NuGet packages
```cmd
dotnet add package Plainquire.Filter
dotnet add package Plainquire.Filter.Mvc
dotnet add package Plainquire.Filter.Swashbuckle
```### 2. Register services
```csharp
using Plainquire.Filter.Mvc;
using Plainquire.Filter.Swashbuckle;var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddFilterSupport();
builder.Services.AddSwaggerGen(options => options.AddFilterSupport());
```### 3. Setup entity
```csharp
using Plainquire.Filter;[EntityFilter(Prefix = "")]
public class Freelancer
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
```### 4. Create HTTP endpoint
```csharp
using Plainquire.Filter;[HttpGet]
public IEnumerable GetFreelancers([FromQuery] EntityFilter filter)
{
var freelancers = GetFreelancersFromDatabase();
var filteredFreelancers = freelancers.Where(filter);
return filteredFreelancers;
}
```### 5. Send HTTP request
```bash
BASE_URL=https://www.plainquire.com/api/Freelancer
curl -O "$BASE_URL/GetFreelancers?firstName=Joe"
```### 6. **Results in SQL statement**
```sql
SELECT *
FROM "Freelancer"
WHERE instr(upper("FirstName"), 'JOE') > 0
```## Usage for non-web applications
### 1. Install NuGet package
```cmd
dotnet add package Plainquire.Filter
```### 2. Setup entity
See [setup entity](#3-setup-entity) from above.
### 3. Create repository
```cs
public IEnumerable GetFreelancers()
{
var filter = new EntityFilter().Add(x => x.FirstName, "~Joe");
var freelancers = GetFreelancersFromDatabase();
var filteredFreelancers = freelancers.Where(filter);
return filteredFreelancers;
}
```# Table of contents
- [Features](#features)
- [Syntax](#syntax)
- [Filter syntax](#filter-syntax)
- [Sort syntax](#sort-syntax)
- [Filter entities](#filter-entities)
- [Basic usage](#basic-usage)
- [Configure filters](#configure-filters)
- [Filter by special values](#filter-by-special-values)
- [Logical Operators](#logical-operators)
- [Nested filters](#nested-filters)
- [Retrieve syntax and filter values](#retrieve-syntax-and-filter-values)
- [REST / MVC](#rest--mvc)
- [Swagger / OpenAPI](#swagger--openapi)
- [Support for Newtonsoft.Json](#support-for-newtonsoftjson)
- [Interception](#interception)
- [Advanced scenarios](#advanced-scenarios)
- [Sort entities](#sort-entities)
- [Basic usage](#basic-usage)
- [Configure sorting](#configure-sorting)
- [Sort entities](#sort-entities)
- [Sort nested entities](#sort-nested-entities)
- [Retrieve syntax and sort direction](#retrieve-syntax-and-sort-direction)
- [REST / MVC](#rest--mvc)
- [Swagger / OpenAPI](#swagger--openapi)
- [Support for Newtonsoft.Json](#support-for-newtonsoftjson)
- [Interception](#interception)
- [Advanced scenarios](#advanced-scenarios)
- [Page Entities](#page-entities)
- [Basic usage](#basic-usage)
- [Configure pagination](#configure-pagination)
- [REST / MVC](#rest--mvc)
- [Swagger / OpenAPI](#swagger--openapi)
- [Support for Newtonsoft.Json](#support-for-newtonsoftjson)
- [Interception](#interception)
- [Advanced Scenarios](#advanced-scenarios)
- [Upgrade from FilterExpressionCreator](#upgrade-from-filterexpressioncreator)# Features
* Filtering, sorting and pagination for ASP.NET Core
* Customizable syntax
* Support for Swagger / OpenUI and code generators via [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
* Support for Entity Framework an other ORM mapper using `IQueryable`
* Support for In-memory lists / arrays via `IEnumerable`
* Binding for HTTP query parameters
* Natural language date/time interpretation (e.g. yesterday, last week Tuesday, ...)
* Filters, sorts and pages are serializable, e.g. to persist user defined filters
* Customizable expressions via interceptors# Syntax
## Filter syntax
**Syntax reference**
The default filter micro syntax uses operator/value pairs separated by `,`, `;`, or `|`
Separated values combined with logical `OR`. To combine values with logical `AND`, specify the filter multiple times.
| Micro syntax | Operator | Description |
| ----------------- | ---------------------- | ------------------------------------------------------------ |
| | `Default` | Selects operator `Contains` for string values; `EqualCaseInsensitive` for others. |
| `~` | `Contains` | Hits when the filtered property contains the filter value |
| `^` | `StartsWith` | Hits when the filtered property starts with the filter value |
| `$` | `EndsWith` | Hits when the filtered property ends with the filter value |
| `=` | `EqualCaseInsensitive` | Hits when the filtered property equals the filter value (case-insensitive) |
| `==` | `EqualCaseSensitive` | Hits when the filtered property equals the filter value (case-sensitive) |
| `!` | `NotEqual` | Negates the `Default` operator. Operators other than `Default` cannot be negated (currently) |
| `<` | `LessThan` | Hits when the filtered property is less than the filter value |
| `<=` | `LessThanOrEqual` | Hits when the filtered property is less than or equal to the filter value |
| `>` | `GreaterThan` | Hits when the filtered property is greater than the filter value |
| `>=` | `GreaterThanOrEqual` | Hits when the filtered property is greater than or equals the filter value |
| `ISNULL` | `IsNull` | Hits when the filtered property is `null` |
| `NOTNULL` | `NotNull` | Hits when the filtered property is not `null` |**Escape filter values**
The backslash (`\`) is the default escape character. To search for a backslash, use a double backslash (`\\`). Escape filter operator characters and separators to include them in searches.
**HTTP query samples**
| Query parameter | Description |
| ------------------------------------------ | ------------------------------------------------------- |
|&gender==female
| Gender equals `female` (case insensitive) |
|&gender==male,=female
| Gender equals `male` OR `female` (case insensitive) |
|&gender===female
| Gender equals `female` (case sensitive) |
|&gender=~male
| Gender contains `male` (fetches `female` too) |
|&tag=ISNULL
| Tag is `null` |
|&tag==
| Tag equals `""` |
|&tag==\=A\;B
| Tag equals `=A;B` (case insensitive due escaped syntax) |
|&size=<100
| Size is lower than `100` |
|&size=>100&size=<200
| Size is between `100` and `200` |
|&created=>two-days-ago
| Created within the last `2 days` |
|&created=yesterday
| Created `yesterday` |
|&created=>2020-03
| Created after `Sunday, March 1, 2020` |
|&created=2020
| Crated in the year `2020` |## Sort syntax
**Syntax reference**
The sort micro syntax includes a property name with an optional sort direction marker (e.g., `customer-asc`). In an HTTP query parameter, you can use a comma-separated list of properties (e.g., `&orderBy=customer,number-desc`).
| Direction | Position | Values |
| ---------- | -------- | -------------------------------------------- |
| ascending | prefix | `+`, `asc-`, `asc ` |
| ascending | postfix | `+`, `-asc`, ` asc` |
| descending | prefix | `-`, `~`, `desc-`, `dsc-`, `desc `, `dsc ` |
| descending | postfix | `-`, `~`, `-desc`, `-dsc`, ` desc`, ` dsc` |**HTTP query samples**
| Query parameter | Description |
| -------------------------------------------------------- | ------------------------------------------------------------ |
|&orderBy=lastName
| Sort by `lastName` ascending |
|&orderBy=lastName-
| Sort by `lastName` descending |
|&orderBy=lastName,-firstName
| Sort by `lastName` ascending, than by `firstName` descending |
|&orderBy=lastName.length
| Sort by `length of lastName` ascending |## Page Syntax
**Syntax reference**
| Micro syntax | Description |
| ----------------- | ---------------------- |
| `page` | The page number to get |
| `pageSize` | The page size to use |**HTTP query samples**
| Query parameter | Description |
| --------------------------------------------- | -------------------------------------- |
|&page=2&pageSize=3
| Takes the 2nd page by a page size of 3 |# Filter entities
## Basic usage
**Install NuGet packages**
```
Package Manager : Install-Package Plainquire.Filter
CLI : dotnet add package Plainquire.Filter
```
**Bind filter from query-parameters**```csharp
using Plainquire.Filter;[HttpGet]
public Task> GetOrders([FromQuery] EntityFilter order)
{
return dbContext.Orders.Where(filter).ToList();
}
```**Create filter from code**
```csharp
using Plainquire.Filter;var orders = new[] {
new Order { Customer = "Joe Miller", Number = 100 },
new Order { Customer = "Joe Smith", Number = 200 },
new Order { Customer = "Joe Smith", Number = 300 },
};// Create filter
var filter = new EntityFilter()
.Add(x => x.Customer, "Joe")
.Add(x => x.Number, FilterOperator.GreaterThan, 250);// Print filter
Console.WriteLine(filter);
// Output: x => (((x.Customer != null) AndAlso x.Customer.ToUpper().Contains("JOE")) AndAlso (x.Number > 250))// Filter using queryables (e.g. Entity Framework)
var filteredOrders = dbContext.Orders.Where(filter).ToList();// Filter using LINQ
var filteredOrders = orders.Where(filter).ToList();
```## Configure filters
Generated filter expressions can be configured via `FilterConfiguration`.
### Create configuration
```csharp
using Plainquire.Filter;// Parse filter values using german locale (e.g. "5,5" => 5.5f).
var configuration = new FilterConfiguration { CultureInfo = new CultureInfo("de-DE") };
```### Provide configuration
```csharp
// For MVC model binding via dependency injection
services.Configure(c => c.IgnoreParseExceptions = true);// Via constructor
new EntityFilter(configuration);// Via static default
FilterConfiguration.Default
```### **Configuration reference**
| Configuration | Description |
| ----------------------- | ------------------------------------------------------------ |
| `CultureName` | Culture used for parsing in `languagecode2-country` - `regioncode2` format (e.g., "en-US"). |
| `UseConditionalAccess` | Controls the use of conditional access to navigation properties |
| `IgnoreParseExceptions` | Fallback to `x => true` if any exception occurs during value parsing |
| `FilterOperatorMap` | Map between micro syntax and operator. Micro syntax is case-sensitive |
| `BooleanMap` | Map between string and boolean. Strings are case-insensitive |
| `ValueSeparatorChars` | Characters used to split values in micro syntax |
| `EscapeCharacter` | Escape character used in micro syntax |## Filter by special values
### Filter by `== null` / `!= null`
```csharp
// For 'Customer is null'
filter.Add(x => x.Customer, FilterOperator.IsNull);
// Output: x => (x.Customer == null)// For 'Customer is not null'
filter.Add(x => x.Customer, FilterOperator.NotNull);
// Output: x => (x.Customer != null)// via query parameter
var getOrdersUrl = "/GetOrders?customer=ISNULL"
var getOrdersUrl = "/GetOrders?customer=NOTNULL"
```While filtered for `== null` / `!= null`, (accidently) given values are ignored:
```csharp
filter.Add(x => x.Customer, FilterOperator.NotNull, "values", "are", "ignored");
```### Filter by `""` / `string.Empty`
```csharp
// For 'Customer == ""'
filter.Add(x => x.Customer, string.Empty);
// Output: x => (x.Customer == "")// For 'Customer is not null'
filter.Add(x => x.Customer, FilterOperator.NotEqual, string.Empty);
// Output: x => (x.Customer != "")// via query parameter
var getOrdersUrl = "/GetOrders?customer="
```### Filter by Date/Time
Date/Time values can be given in the form of a fault-tolerant [round-trip date/time pattern](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip)
```csharp
// Date
filter.Add(x => x.Created, ">2020/01/01");
// Output: x => (x.Created > 01.01.2020 00:00:00)// Date/Time
filter.Add(x => x.Created, ">2020-01-01-12-30");
// Output: x => (x.Created > 01.01.2020 12:30:00)// Partial values are supported too
filter.Add(x => x.Created, "2020-01");
// Output: x => ((x.Created >= 01.01.2020 00:00:00) AndAlso (x.Created < 01.02.2020 00:00:00))
```### Filter by Date/Time with natural language
Thanks to [nChronic.Core](https://github.com/robbell/nChronic.Core) natural language for date/time is supported.
```csharp
// This
filter.Add(x => x.Created, ">yesterday");// works as well as
filter.Add(x => x.Created, ">3-months-ago-saturday-at-5-pm");
```Details can be found here: [https://github.com/mojombo/chronic](https://github.com/mojombo/chronic#simple)
### Filter by enum
`Enum` values can be filtered by its name as well as by it's numeric representation.
```csharp
// Equals by name
filter.Add(x => x.Gender, "=divers");
// Output: x => (x.Gender == Divers)// Equals by numeric value
filter.Add(x => x.Gender, "=1");
// Output: x => (Convert(x.Gender, Int64) == 1)// Contains, value is expanded
filter.Add(x => x.Gender, "~male");
// Output: x => ((x.Gender == Male) OrElse (x.Gender == Female))enum Gender { Divers, Male, Female }
```### Filter by numbers
Filter for numbers support `contains` operator but may be less performant.
```csharp
// Equals
filter.Add(x => x.Number, "1");
// Output: x => (x.Number == 1)// Contains
filter.Add(x => x.Number, "~1");
// Output: x => x.Number.ToString().ToUpper().Contains("1")
```## Logical Operators
### Add/Replace filter using logical OR
Multiple values given to one call are combined using conditional `OR`.
```csharp
// Customer contains `Joe` || `Doe`var filter = new EntityFilter();
// via operator
filter.Add(x => x.Customer, FilterOperator.Contains, "Joe", "Doe");
filter.Replace(x => x.Customer, FilterOperator.Contains, "Joe", "Doe");// via syntax
filter.Add(x => x.Customer, "~Joe,~Doe");
filter.Replace(x => x.Customer, "~Joe,~Doe");// via query parameter
var getOrdersUrl = "/GetOrders?customer=~Joe,~Doe"
```### Add/Replace filter using logical AND
Multiple calls are combined using conditional `AND`.
```csharp
// Customer contains `Joe` && `Doe`var filter = new EntityFilter();
// via operator
filter
.Add(x => x.Customer, FilterOperator.Contains, "Joe")
.Add(x => x.Customer, FilterOperator.Contains, "Doe");// via syntax
filter
.Add(x => x.Customer, "~Joe")
.Add(x => x.Customer, "~Doe");// via query parameter
var getOrdersUrl = "/GetOrders?customer=~Joe&customer=~Doe"
```## Nested filters
Nested objects are filtered directly (`x => x.Address.City == "Berlin"`)
Nested lists are filtered using `.Any()` (`x => x.Items.Any(item => (item.Article == "Laptop"))`)
```csharp
()
// Create filters
var addressFilter = new EntityFilter
.Add(x => x.City, "==Berlin");var itemFilter = new EntityFilter()
.Add(x => x.Article, "==Laptop");var orderFilter = new EntityFilter()
.AddNested(x => x.Address, addressFilter)
.AddNested(x => x.Items, itemFilter);// Print filter
Console.WriteLine(orderFilter);
// Output:
// x => ((x.Address != null) AndAlso (x.Address.City == "Berlin"))
// x => ((x.Items != null) AndAlso x.Items.Any(x => (x.Article == "Laptop")))public class Order
{
public int Number { get; set; }
public string Customer { get; set; }public Address Address { get; set; }
public List Items { get; set; }
}public record Address(string Street, string City);
public record OrderItem(int Position, string Article);
```## Retrieve syntax and filter values
```csharp
var filter = new EntityFilter()
.Add(x => x.Customer, FilterOperator.Contains, "Joe", "Doe");// Retrive filter syntax
string filterSytax = filter.GetPropertyFilterSyntax(x => x.Customer);
// Output: ~Joe,~Doe// Retrive filter values
ValueFilter[] filterValues = filter.GetPropertyFilterValues(x => x.Customer);
// Output:
// [{
// "Operator": "Contains",
// "Value": "Joe",
// "IsEmpty": false
// }, {
// "Operator": "Contains",
// "Value": "Doe",
// "IsEmpty": false
// }]
```## REST / MVC
To filter an entity via model binding, the entity must be marked with `EntityFilterAttribute`
### Register model binders
```
Package Manager : Install-Package Plainquire.Filter.Mvc
CLI : dotnet add package Plainquire.Filter.Mvc
``````csharp
using Plainquire.Filter.Mvc;// Register required stuff by calling 'AddFilterSupport()' on IMvcBuilder instance
services.AddControllers().AddFilterSupport();
```### Map HTTP query parameters to `EntityFilter`
With model binding enabled, REST requests can be filtered using query parameters:
```csharp
using Plainquire.Filter;var getOrdersUrl = "/GetOrders?customer==Joe&number=>4711"
[HttpGet]
public Task> GetOrders([FromQuery] EntityFilter filter)
{
Console.WriteLine(filter);
// Output:
// x => (
// ((x.Customer != null) AndAlso (x.Customer.ToUpper() == "JOE"))
// AndAlso (x.Number > 4711)
// )var queryParams = filter.ToQueryParams();
// Output: customer==Joe&number=>4711
}
```### Configure model binding
By default, parameters for properties of filtered entity are named `{Entity}{Property}`.
By default, all public non-complex properties (`string`, `int`, `DateTime`, ...) are recognized.
Parameters can be renamed or removed using `FilterAttribute` and `EntityFilterAttribute`.For the code below `Number` is not mapped anymore and `Customer` becomes `CustomerName`:
```csharp
using Plainquire.Filter.Abstractions;// Remove prefix, e.g. property 'Number' is mapped from 'number', not 'orderNumber'
[EntityFilter(Prefix = "")]
public class Order
{
// 'Number' is removed from filter and will be ignored
[Filter(Filterable = false)]
public int Number { get; set; }// 'Customer' is mapped from query-parameter 'customerName'
[Filter(Name = "CustomerName")]
public string Customer { get; set; }
}
```
### Filter setsMultiple entity filters can be combined to a set of filters using the `EntityFilterSetAttribute`.
```csharp
using Plainquire.Filter;
using Plainquire.Filter.Abstractions;// Use
[HttpGet]
public Task> GetOrders([FromQuery] OrderFilterSet filterSet)
{
var order = filterSet.Order;
var orderItem = filterSet.OrderItem;
}// Instead of
public Task> GetOrders([FromQuery] EntityFilter order, EntityFilter orderItem) { ... }[EntityFilterSet]
public class OrderFilterSet
{
public EntityFilter Order { get; set; }
public EntityFilter OrderItem { get; set; }
}
```
## Swagger / OpenAPI
### Register OpenAPI support
Swagger / OpenAPI is supported when using [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
```
Package Manager : Install-Package Plainquire.Filter.Swashbuckle
CLI : dotnet add package Plainquire.Filter.Swashbuckle
``````csharp
using Plainquire.Filter.Swashbuckle;services.AddSwaggerGen(options =>
{
// Register filters used to modify swagger.json
options.AddFilterSupport();
});
```### Register XML documentation
To get descriptions for generated parameters from XML documentation, paths to documentation files can be provided.
```csharp
services.AddSwaggerGen(options =>
{
var filterDoc = Path.Combine(AppContext.BaseDirectory, "Plainquire.Filter.xml");
options.AddFilterSupport(filterDoc);
options.IncludeXmlComments(filterDoc);
});
```## Support for Newtonsoft.Json
By default `System.Text.Json` is used to serialize/convert Plainquire specific stuff. If you like to use Newtonsoft.Json you must register it:
```
Package Manager : Install-Package Plainquire.Filter.Mvc.Newtonsoft
CLI : dotnet add package Plainquire.Filter.Mvc.Newtonsoft
``````csharp
using Plainquire.Filter.Mvc.Newtonsoft;// Register support for Newtonsoft by calling
// 'AddFilterNewtonsoftSupport()' on IMvcBuilder instance
services.AddControllers().AddFilterNewtonsoftSupport();
```## Interception
Creation of filter expression can be intercepted via `IFilterInterceptor`. While implicit conversions to `Func` and `Expression>` exists, explicit filter conversion is required to apply an interceptor.
```csharp
var filter = new EntityFilter();
var interceptor = new FilterStringsCaseInsensitiveInterceptor();
var filterExpression = filter.CreateFilter(interceptor) ?? (x => true);
var filteredList = orders.Where(filterExpression.Compile());
var filteredDb = _dbContext.Orders.Where(filterExpression);
```### Default interceptor
A default interceptor can be provided via static `IFilterInterceptor.Default`.
### Sample interceptor
Interceptor to omit filter values having an empty value. Allows to omit filters added by empty query parameters (`&birthday=`) but prevents filtering for empty strings (`&name=`).
```csharp
public class OmitEmptyFilterInterceptor : IFilterInterceptor
{
public Expression>? CreatePropertyFilter(PropertyInfo propertyInfo, IEnumerable filters, FilterConfiguration configuration)
{
var nonEmptyFilters = filters.Where(ValueIsNotNullOrEmpty).ToList();var noFilterRequired = nonEmptyFilters.Count == 0;
return noFilterRequired
? PropertyFilterExpression.EmptyFilter()
: PropertyFilterExpression.CreateFilter(propertyInfo, nonEmptyFilters, configuration, this);
}private static bool ValueIsNotNullOrEmpty(ValueFilter valueFilter)
=> !string.IsNullOrEmpty(valueFilter.Value);Func IFilterInterceptor.Now => () => DateTimeOffset.Now;
}
```## Advanced scenarios
### Deep copy
The `EntityFilter` class supports deep cloning by calling the `Clone()` method
```csharp
var copy = filter.Clone();
```### Casting
Filters can be cast between entities, e.g. to convert them between DTOs and database models.
Properties are matched by type (check if assignable) and name (case-sensitive)
```csharp
var dtoFilter = new EntityFilter().Add(...);
var orderFilter = dtoFilter.Cast();
```### Serialization
#### Using `System.Text.Json`
Objects of type `EntityFilter` can be serialized via `System.Text.Json.JsonSerializer` without further requirements
```csharp
var json = JsonSerializer.Serialize(filter);
filter = JsonSerializer.Deserialize>(json);
```#### Using `Newtonsoft.Json`
When using `Newtonsoft.Json` additional converters are required
```
Package Manager : Install-Package Plainquire.Filter.Newtonsoft
CLI : dotnet add package Plainquire.Filter.Newtonsoft
``````csharp
using Plainquire.Filter.Newtonsoft;var json = JsonConvert.SerializeObject(filter, JsonConverterExtensions.NewtonsoftConverters);
filter = JsonConvert.DeserializeObject>(json, JsonConverterExtensions.NewtonsoftConverters);
```### Combine filter expressions
To add custom checks to a filter either call `.Where(...)` again
```csharp
var filteredOrders = orders
.Where(filter)
.Where(item => item.Items.Count > 2);
```or where this isn't possible combine filters with `CombineWithConditionalAnd`
```csharp
using Plainquire.Filter.Abstractions;var extendedFilter = new[]
{
filter.CreateFilter(),
item => item.Items.Count > 2
}
.CombineWithConditionalAnd();var filteredOrders = orders.Where(extendedFilter.Compile());
```# Sort entities
## Basic usage
**Install NuGet packages**
```
Package Manager : Install-Package Plainquire.Sort
CLI : dotnet add package Plainquire.Sort
```
**Create a sort**```csharp
using Plainquire.Sort;var orders = new[] {
new Order { Customer = "Joe Miller", Number = 100 },
new Order { Customer = "Joe Smith", Number = 200 },
new Order { Customer = "Joe Smith", Number = 300 },
};// Create sort
var sort = new EntitySort()
.Add(x => x.Customer, SortDirection.Ascending)
.Add(x => x.Number, SortDirection.Descending);// Print sort
Console.WriteLine($"{orders.OrderBy(sort)}");
// Output: orders.OrderBy(x => IIF((x == null), null, x.Customer)).ThenByDescending(x => x.Number)// Use sort with LINQ
var sortedOrders = orders.OrderBy(sort).ToList();
// Or queryables (e.g. Entity Framework)
var sortedOrders = dbContext.Orders.OrderBy(sort).ToList();[EntityFilter]
public class Order
{
public int Number { get; set; }
public string Customer { get; set; }
}
```**Or bind sort from query-parameters**
```csharp
using Plainquire.Sort;[HttpGet]
public Task> GetOrders([FromQuery] EntitySort sort)
{
return dbContext.Orders.OrderBy(sort).ToList();
}
```## Configure sorting
Generated sort expression can be configured via `SortConfiguration`.
### Create configuration
```csharp
using Plainquire.Sort.Abstractions;var configuration = new SortConfiguration();
configuration.AscendingPostfixes.Add("^");
```### Provide configuration
```csharp
// For MVC model binding via dependency injection
services.Configure(c => c.IgnoreParseExceptions = true);// Via constructor
new EntitySort(configuration);// Via static default
SortConfiguration.Default
```### **Configuration reference**
| Configuration | Description |
| --------------------------------- | ------------------------------------------------------------ |
| `AscendingPrefixes` | Prefixes used to identify an ascending sort order |
| `AscendingPostfixes` | Postfixes used to identify an ascending sort order |
| `DescendingPrefixes` | Prefixes used to identify a descending sort order |
| `DescendingPostfixes` | Postfixes used to identify a descending sort order |
| `IgnoreParseExceptions` | Fallback to `source.OrderBy(x => 0)` if any exception occurs during value parsing |
| `UseConditionalAccess` | Controls the use of conditional access to navigation properties (e.g. `person => person?.Name`) |
| `CaseInsensitivePropertyMatching` | Indicates whether to use case-insensitive property matching |## Sort entities
```csharp
// Order is sorted by `Address` ascending.
var sort = new EntitySort();// via operator
sort.Add(x => x.Address, SortDirection.Ascending);// via syntax
sort.Add("Address-asc")// via query parameter
var getOrdersUrl = "/GetOrders?orderBy=customer-asc"
```## Sort nested entities
Nested objects are sorted directly (`x=> x.OrderBy(order => order.Customer)`).
Deep property paths (e.g. `order => order.Customer.Length`) are supported.
Methods calls (e.g. `order => order.Customer.SubString(1)`) are not supported for security reasons.Nested lists cannot be sorted directly. You can create an own `EntitySort` for it and sort the nested list by.
```csharp
()
// Create sort
var addressSort = new EntitySort
.Add(x => x.City);// AddNested() is equivalent to adding the paths directly
var orderSort = new EntitySort()
.AddNested(x => x.Address, addressSort);// Is equivalent to AddNested() above
var orderSort = new EntitySort()
.Add(x => x.Address.City, SortDirection.Ascending);// Print sort
Console.WriteLine(orders.OrderBy(orderSort).ToString());
// Output:
// orders => orders.OrderBy(x => IIF((IIF((x == null), null, x.Address) == null), null, x.Address.City))public class Order
{
public int Number { get; set; }
public string Customer { get; set; }
public Address Address { get; set; }
}public record Address(string Street, string City);
```## Retrieve syntax and sort direction
```csharp
var orderSort = new EntitySort()
.Add(x => x.Customer, SortDirection.Ascending);// Retrive sort syntax
var syntax = orderSort.GetPropertySortSyntax(x => x.Customer);
// Output: Customer-asc// Retrive sort direction
var direction = orderSort.GetPropertySortDirection(x => x.Customer);
// Output: Ascending// Retrive sort expression string:
var orderExpression = orders.OrderBy(orderSort).ToString()
```## REST / MVC
To sort an entity via model binding, the entity must be marked with `EntityFilterAttribute`
### Register model binders
```
Package Manager : Install-Package Plainquire.Sort.Mvc
CLI : dotnet add package Plainquire.Sort.Mvc
``````csharp
using Plainquire.Sort.Mvc;// Register required stuff by calling 'AddSortSupport()' on IMvcBuilder instance
services.AddControllers().AddSortSupport();
```### Map HTTP query parameter to `EntitySort`
With model binding enabled, REST requests can be sorted using query parameter `orderBy`.
```csharp
using Plainquire.Sort;var getOrdersUrl = "/GetOrders?orderBy=customer,number-desc"
[HttpGet]
public Task> GetOrders([FromQuery] EntitySort sort)
{
var orders = new List();
var sortedOrders = orders.OrderBy(sort);
Console.WriteLine($"{sortedOrders.OrderBy(sort)}");
// Output: orders.OrderBy(x => IIF((x == null), null, x.Customer)).ThenByDescending(x => x.Number)var queryParams = sort.ToString();
// Output: Customer-asc, Number-desc
}
```### Configure model binding
By default, parameters for properties of sorted entity are named `{Entity}{Property}`.
By default, all public non-complex properties (`string`, `int`, `DateTime`, ...) are recognized.
Parameters can be renamed or removed using `FilterAttribute` and `EntityFilterAttribute`.For the code below `Number` is not mapped anymore and `Customer` becomes `CustomerName`.
```csharp
using Plainquire.Filter.Abstractions;// Remove prefix, e.g. property 'Number' is mapped from 'number', not 'orderNumber'
// Use 'sortBy' as query parameter name instead of default 'orderBy'
[EntityFilter(Prefix = "")]
public class Order
{
// 'Number' is removed from sort and will be ignored
[Filter(Sortable = false)]
public int Number { get; set; }// 'Customer' is mapped from query-parameter 'customerName'
[Filter(Name = "CustomerName")]
public string Customer { get; set; }
}
```
### Order setsMultiple entity sorts can be combined to a set of filters using the `EntitySortSetAttribute`.
```csharp
using Plainquire.Sort;
using Plainquire.Sort.Abstractions;// Use
[HttpGet]
public Task> GetOrders([FromQuery] OrderSortSet orderSet)
{
var orderSort = orderSet.Order;
var orderItemSort = orderSet.OrderItem;
}// Instead of
public Task> GetOrders([FromQuery] EntitySort orderSort, EntitySort orderItemSort) { ... }[EntitySortSet]
public class OrderSortSet
{
public EntitySort Order { get; set; }
public EntitySort OrderItem { get; set; }
}
```
## Swagger / OpenAPI
### Register OpenAPI support
Swagger / OpenAPI is supported when using [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
```
Package Manager : Install-Package Plainquire.Sort.Swashbuckle
CLI : dotnet add package Plainquire.Sort.Swashbuckle
``````csharp
using Plainquire.Sort.Swashbuckle;services.AddSwaggerGen(options =>
{
// Register filters used to modify swagger.json
options.AddSortSupport();
});
```## Support for Newtonsoft.Json
By default, `System.Text.Json` is used to serialize/convert Plainquire specific stuff. If you like to use Newtonsoft.Json you must register it.
```
Package Manager : Install-Package Plainquire.Sort.Mvc.Newtonsoft
CLI : dotnet add package Plainquire.Sort.Mvc.Newtonsoft
``````csharp
using Plainquire.Sort.Mvc.Newtonsoft;// Register support for Newtonsoft by calling
// 'AddSortNewtonsoftSupport()' on IMvcBuilder instance
services.AddControllers().AddSortNewtonsoftSupport();
```## Interception
Creation of sort expression can be intercepted via `ISortInterceptor`.
```csharp
var sort = new EntitySort();
var interceptor = new CaseInsensitiveSortInterceptor();
var filtered = orders.OrderBy(sort, interceptor);
```A default interceptor can be provided via static `ISortInterceptor.Default`.
## Advanced scenarios
### Deep copy
The `EntitySort` class supports deep cloning by calling the `Clone()` method
```csharp
var copy = sort.Clone();
```### Casting
Sorting can be cast between entities, e.g. to convert them between DTOs and database models.
Properties are matched by type (check if assignable) and name (case-sensitive)
```csharp
var dtoSort = new EntitySort().Add(...);
var orderSort = dtoSort.Cast();
```### Serialization
#### Using `System.Text.Json`
Objects of type `EntitySort` can be serialized via `System.Text.Json.JsonSerializer` without further requirements
```csharp
var json = JsonSerializer.Serialize(sort);
sort = JsonSerializer.Deserialize>(json);
```#### Using `Newtonsoft.Json`
When using `Newtonsoft.Json` additional converters are required
```
Package Manager : Install-Package Plainquire.Sort.Newtonsoft
CLI : dotnet add package Plainquire.Sort.Newtonsoft
``````csharp
using Plainquire.Sort.Newtonsoft;var json = JsonConvert.SerializeObject(sort, JsonConverterExtensions.NewtonsoftConverters);
sort = JsonConvert.DeserializeObject>(json, JsonConverterExtensions.NewtonsoftConverters);
```# Page Entities
## Basic usage
**Install NuGet packages**
```
Package Manager : Install-Package Plainquire.Page
CLI : dotnet add package Plainquire.Page
```
**Create a page**```csharp
using Plainquire.Page;// Direct pageing is the preferred way
var pagedOrders = orders.Page(pageNumber: 2, pageSize: 3).ToList();// Alternative, create a EntityPage object
var page = new EntityPage(pageNumber: 2, pageSize: 3);// Use page with LINQ
var pagedOrders = orders.Page(page).ToList();
// Or queryables (e.g. Entity Framework)
var pagedOrders = dbContext.Orders.Page(page).ToList();```
## Configure pagination
### Create configuration
```csharp
using Plainquire.Page.Abstractions;var configuration = new PageConfiguration() { IgnoreParseExceptions = true };
```### Provide configuration
```csharp
// For MVC model binding via dependency injection
services.Configure(c => c.IgnoreParseExceptions = true);// Via constructor
new EntityPage(configuration);// Via static default
PageConfiguration.Default
```### **Configuration reference**
| Configuration | Description |
| ----------------------- | ------------------------------------------------------------ |
| `IgnoreParseExceptions` | Omit paging in case of any exception while parsing the value |## REST / MVC
To page an entity via model binding, the entity must be marked with `EntityFilterAttribute`
### Register model binders
```
Package Manager : Install-Package Plainquire.Page.Mvc
CLI : dotnet add package Plainquire.Page.Mvc
``````csharp
using Plainquire.Page.Mvc;// Register required stuff by calling 'AddPageSupport()' on IMvcBuilder instance
services.AddControllers().AddPageSupport();
```### Map HTTP query parameter to `EntityPage`
With model binding enabled, REST requests can be paged using query parameters `page` and `pageSize`.
```csharp
using Plainquire.Page;var getOrdersUrl = "/GetOrders?page=2&pageSize=3"
[HttpGet]
public Task> GetOrders([FromQuery] EntityPage page)
{
return dbContext.Orders.Page(page).ToList();
}
```### Configure model binding
Parameters can be renamed `EntityFilterAttribute`.
For the code below page number is taken from query parameter `pageNumber` and page size from `size`.
```csharp
using Plainquire.Filter.Abstractions;[EntityFilter(PageNumberParameter = "pageNumber", PageSizeParameter = "size")]
public class Order
{
public string Customer { get; set; }
}
```
## Swagger / OpenAPI
### Register OpenAPI support
Swagger / OpenAPI is supported when using [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
```
Package Manager : Install-Package Plainquire.Page.Swashbuckle
CLI : dotnet add package Plainquire.Page.Swashbuckle
``````csharp
using Plainquire.Page.Swashbuckle;services.AddSwaggerGen(options =>
{
// Register filters used to modify swagger.json
options.AddPageSupport();
});
```## Support for Newtonsoft.Json
By default, `System.Text.Json` is used to serialize/convert Plainquire specific stuff. If you like to use Newtonsoft.Json you must register it.
```
Package Manager : Install-Package Plainquire.Page.Mvc.Newtonsoft
CLI : dotnet add package Plainquire.Page.Mvc.Newtonsoft
``````csharp
using Plainquire.Page.Mvc.Newtonsoft;// Register support for Newtonsoft by calling
// 'AddPageNewtonsoftSupport()' on IMvcBuilder instance
services.AddControllers().AddPageNewtonsoftSupport();
```## Interception
Creation of page expression can be intercepted via `IPageInterceptor`.
```csharp
var page = new EntityPage();
var interceptor = new PageBackwardInterceptor();
var paged = orders.Page(page, interceptor);
```A default interceptor can be provided via static `IPageInterceptor.Default`.
## Advanced Scenarios
### Deep copy
The `EntityPage` class supports deep cloning by calling the `Clone()` method
```csharp
var copy = page.Clone();
```### Serialization
#### Using `System.Text.Json`
Objects of type `EntityPage` can be serialized via `System.Text.Json.JsonSerializer` without further requirements
```csharp
var json = JsonSerializer.Serialize(page);
page = JsonSerializer.Deserialize>(json);
```#### Using `Newtonsoft.Json`
When using `Newtonsoft.Json` additional converters are required
```
Package Manager : Install-Package Plainquire.Page.Newtonsoft
CLI : dotnet add package Plainquire.Page.Newtonsoft
``````csharp
using Plainquire.Page.Newtonsoft;var json = JsonConvert.SerializeObject(page, JsonConverterExtensions.NewtonsoftConverters);
sort = JsonConvert.DeserializeObject>(json, JsonConverterExtensions.NewtonsoftConverters);
```# Upgrade from FilterExpressionCreator
* Install `Schick.FilterExpressionCreator*` 4.7.x.
* Fix all warnings. This can largely be done by sear and replacing with regular expressions
* Search for: `FS.FilterExpressionCreator(\.Abstractions|Mvc|Mvc\.Newtonsoft|Newtonsoft)?(\.\w+)?`
* Replace with: `Plainquire.Filter$1`
* Search for: `\[FilterEntity(\(.*\))]`
* Replace with: `[EntityFilter$1]`
* Fix remaining errors and warnings following description of [breaking changes](https://github.com/plainquire/plainquire/blob/main/HISTORY.md#breaking-changes)
* Uninstall all `Schick.FilterExpressionCreator*` stuff
* Install corresponding `Plainquire.*` packages