zackattackz.FilterExpressionCreator 5.0.0-alpha.2

This is a prerelease version of zackattackz.FilterExpressionCreator.
dotnet add package zackattackz.FilterExpressionCreator --version 5.0.0-alpha.2                
NuGet\Install-Package zackattackz.FilterExpressionCreator -Version 5.0.0-alpha.2                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="zackattackz.FilterExpressionCreator" Version="5.0.0-alpha.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add zackattackz.FilterExpressionCreator --version 5.0.0-alpha.2                
#r "nuget: zackattackz.FilterExpressionCreator, 5.0.0-alpha.2"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install zackattackz.FilterExpressionCreator as a Cake Addin
#addin nuget:?package=zackattackz.FilterExpressionCreator&version=5.0.0-alpha.2&prerelease

// Install zackattackz.FilterExpressionCreator as a Cake Tool
#tool nuget:?package=zackattackz.FilterExpressionCreator&version=5.0.0-alpha.2&prerelease                

C# / VB / .NET LINQ Filter Expression Creator

Dynamically creates lambda expressions to filter enumerable and database queries via System.Linq.Enumerable.Where(...).

Demo

Demo: https://filterexpressioncreator.schick-software.de/demo

API: https://filterexpressioncreator.schick-software.de/openapi/

Table of Content

Getting Started

  1. Install the standard NuGet package into your ASP.NET Core application
Package Manager : Install-Package Schick.FilterExpressionCreator
CLI : dotnet add package Schick.FilterExpressionCreator
  1. Create a new filter
using FS.FilterExpressionCreator.Enums;
using FS.FilterExpressionCreator.Extensions;
using FS.FilterExpressionCreator.Filters;

public class Order
{
    public int Number { get; set; }
    public string Customer { get; set; }
}

var filter = new EntityFilter<Order>()
    .Add(x => x.Number, FilterOperator.GreaterThan, 200)
    .Add(x => x.Customer, "Joe");

// Show filter
System.Console.WriteLine(filter);
// x => (((x.Customer != null) AndAlso x.Customer.ToUpper().Contains("JOE")) AndAlso (x.Number > 200))
  1. Start filtering
using FS.FilterExpressionCreator.Enums;
using FS.FilterExpressionCreator.Extensions;
using FS.FilterExpressionCreator.Filters;
using System.Linq;

var orders = new[] {
    new Order { Customer = "Joe Miller", Number = 100 },
    new Order { Customer = "Joe Smith", Number = 200 },
    new Order { Customer = "Joe Smith", Number = 300 },
};

// Using IEnumerable<>
var filteredOrders = orders.Where(filter).ToList();
// filteredOrders equals
// new[] { new Order { Customer = "Joe Smith", Number = 300 } };

// Or using queryables (e.g. Entity Framework)
var filteredOrders = dbContext.Orders.Where(filter).ToList();

Creating Filters

General

Filters can be added/replaced using operator/value(s) pairs

// Operator/Value(s): Customer contains 'Joe' or 'Doe'
filter.Add(x => x.Customer, FilterOperator.Contains, "Joe", "Doe");

// Operator/Value(s): Customer contains 'Joe' or equals 'Doe'
filter.Add(x => x.Customer, ValueFilter.Create(
    FilterOperator.EqualCaseInsensitive, "Joe"), 
    ValueFilter.Create(FilterOperator.EqualCaseInsensitive, "Doe")
);

or via filter micro syntax

// Filter micro syntax: Customer contains 'Joe' or 'Doe'.
filter.Add(x => x.Customer, "~Joe,~Doe");

// Filter micro syntax: Customer contains 'Joe' or equals 'Doe'
filter.Add(x => x.Customer, "~Joe,=Doe");

Combine values with AND and OR

// OR: Customer contains 'Joe' or 'Doe'.
// Multiple values given to one call of Add/Replace are combined using conditional `OR`.
filter.Add(x => x.Customer, "~Joe,~Doe");

// AND: Customer contains 'Joe' and 'Doe'.
// Multiple calls to Add/Replace are combined using conditional `AND`.
filter.Add(x => x.Customer, "~Joe");
filter.Add(x => x.Customer, "~Doe");

Nested Filters

Filtering nested objects/lists is supported.

Nested Objects

Nested objects are filtered directly

using FS.FilterExpressionCreator.Enums;
using FS.FilterExpressionCreator.Extensions;
using FS.FilterExpressionCreator.Filters;
using System.Collections.Generic;

public class Order
{
    public int Number { get; set; }
    public string Customer { get; set; }
    
    public Address Address { get; set; }
    public List<OrderItem> Items { get; set; }
}

public record Address(string Street, string City) { }
public record OrderItem(int Position, string Article) { }

var addressFilter = new EntityFilter<Address>()
    .Add(x => x.City, "==Berlin");

var filter = new EntityFilter<Order>()
    .Add(x => x.Address, addressFilter);

// Print
System.Console.WriteLine(filter);
// x => ((x.Address != null) AndAlso (x.Address.City == "Berlin"))

Nested Lists

Nested lists are filtered using IEnumerable<T>.Any()

var itemFilter = new EntityFilter<OrderItem>()
    .Add(x => x.Article, "==Laptop");

var filter = new EntityFilter<Order>()
    .Add(x => x.Items, itemFilter);

// Print
System.Console.WriteLine(filter);
// x => ((x.Items != null) AndAlso x.Items.Any(x => (x.Article == "Laptop")))

Filter Operators

Operator Micro Syntax Description
Default Selects the operator according to the filtered type. When filtering string the default is Contains; otherwise EqualCaseInsensitive
Contains ~ Hits when the filtered property contains 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 can not 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

Filter to null

To filter to == null / != null special filter operators exists

// Filtering for values are NULL
filter.Add(x => x.Customer, FilterOperator.IsNull);

// Filtering for values are NOT NULL
filter.Add(x => x.Customer, FilterOperator.NotNull, "values", "are", "ignored");

While filtering for == null / != null, given values are ignored.

Filter Micro Syntax

The filter syntax consists of an operator shortcut (see filter operators above) and a list of comma separated values. When a value contains a comma itself, it must be escaped by a backslash. The backslash itself is escaped by another backslash.

Examples

Syntax Description
Joe For string filtered value contains 'Joe', for Enum filtered value is 'Joe'
~Joe Filtered value contains 'Joe', even for Enum
~1,2 Filtered value contains 1 or 2
=1\,2 Filtered value equals '1,2'
<4,>10 Filtered value is less than 4 or greater than 10
~Joe<br />~Doe Filtered value contains Joe and Doe
ISNULL Filtered value is null
>one-week-ago For DateTime filtered value is greater than one week ago, for others types see above
2020 For DateTime filtered value is between 01/01/2020 and 12/31/2020, for others types see above
2020-01 For DateTime filtered value is between 01/01/2020 and 1/31/2020, for others types see above

Date/Time

Date/Time values can be given as a representation in the form of a fault-tolerant round-trip date/time pattern

2020/01/01 works as well as 2020-01-01-12-30

or thanks to nChronic.Core as natural language date string. More details can be found here: https://github.com/mojombo/chronic

tomorrow works as well as 3-months-ago-saturday-at-5-pm

Partial date strings are supported too

2020-01 filters date between 2020-01-01 and 2020-01-31.

Enumerations

Enum values can be filtered by it's numeric representation as well as by it's name.

When filtering using Contains the filter values are expanded ("~male" filters for male as well as female).

Configuration

Creation of filter expression can be configured via FilterConfiguration. While implicit conversions to Func<TEntity, bool> and Expression<Func<TEntity, bool>> exists, explicit filter conversion is required to apply a configuration

using FS.FilterExpressionCreator.Models;

// Parse filter values using german locale (e.g. "5,5" => 5.5f).
var configuration = new FilterConfiguration { CultureInfo = new CultureInfo("de-DE") };
// Explicit filter conversion
var filterExpression = filter.CreateFilter(configuration);
// Filter IEnumerable<T> by compiling filter expression
var filteredOrders = orders.Where(filterExpression.Compile()).ToList();

Interception

Creation of filter expression can be intercepted via IPropertyFilterInterceptor. While implicit conversions to Func<TEntity, bool> and Expression<Func<TEntity, bool>> exists, explicit filter conversion is required to apply an interceptor.

An example can be found in the test code InterceptorTests

Default Configuration and Interception

The class EntityFilter has static properties to provide a system-wide configuration and/or interceptors

/// <summary>
/// Gets or sets the default configuration. Can be used to set a system-wide configuration.
/// </summary>
public static FilterConfiguration DefaultConfiguration { get; set; } = new FilterConfiguration();

/// <summary>
/// Gets or sets the default interceptor. Can be used to set a system-wide interceptor.
/// </summary>
public static IPropertyFilterInterceptor DefaultInterceptor { get; set; }

Using With MVC Controllers

Model Binding

Model binding for MVC controllers is supported

using FS.FilterExpressionCreator.Filters;

[HttpGet]
public Task<List<Order>> GetOrders([FromQuery] EntityFilter<Order> order, ...) { }

The above usage maps the query parameters OrderNumber and OrderCustomer to the filter parameter order.

Filter Sets

Multiple entity filters can be combined to a set of filters using the EntityFilterSetAttribute.

Instead of

using FS.FilterExpressionCreator.Filters;

[HttpGet]
public Task<List<Order>> GetOrders([FromQuery] EntityFilter<Order> order, EntityFilter<OrderItem> orderItem, ...) { }

you can write

using FS.FilterExpressionCreator.Filters;
using FS.FilterExpressionCreator.Abstractions.Attributes;

[HttpGet]
public Task<List<Order>> GetOrders([FromQuery] OrderFilter filter, ...)
{ 
    var order = filter.Order;
    var orderItem = filter.OrderItem;
}

[EntityFilterSet]
public class OrderFilter
{
	public EntityFilter<Order> Order { get; set; }
	public EntityFilter<OrderItem> OrderItem { get; set; }
}

Register Model Binders

Package Manager : Install-Package Schick.FilterExpressionCreator.Mvc
CLI : dotnet add package Schick.FilterExpressionCreator.Mvc
using FS.FilterExpressionCreator.Mvc.Extensions;
// Register required stuff by calling 'AddFilterExpressionsSupport()' on IMvcBuilder instance
services.AddControllers().AddFilterExpressionsSupport();

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 hidden using FilterAttribute and FilterEntityAttribute. For the code below OrderNumber is not mapped anymore and OrderCustomer becomes CustomerName

using FS.FilterExpressionCreator.Abstractions.Attributes;

[FilterEntity(Prefix = "")]
public class Order
{
    [Filter(Visible = false)]
    public int Number { get; set; }

    [Filter(Name = "CustomerName")]
    public string Customer { get; set; }
}

Nested Objects/Lists

Nested objects and lists are not implicit recognized as parameters. But you can simply combine them

[HttpGet]
public Task<List<Order>> GetOrders(
    [FromQuery] EntityFilter<Order> order, 
    [FromQuery] EntityFilter<Address> address
)
{
    var filter = order.Add(x => x.Address, address);
    // ...
}

Retrieve HTTP query parameters from filter

Use the extension method ToQueryParams() to convert an EntityFilter<TEntity> back to it's HTTP query parameters

var filter = new EntityFilter<Order>()
    .Add(x => x.Number, FilterOperator.GreaterThan, 200)
    .Add(x => x.Customer, "Joe");

var queryParams = filter.ToQueryParams();

// Print query parameters
System.Console.WriteLine(filter);
// number=>200&customer=Joe

Support for OpenAPI / Swashbuckle.AspNetCore

Support for OpenAPI (formerly swagger) provided by Swashbuckle.AspNetCore is available.

Register OpenAPI Support

Package Manager : Install-Package Schick.FilterExpressionCreator.Swashbuckle
CLI : dotnet add package Schick.FilterExpressionCreator.Swashbuckle
using FS.FilterExpressionCreator.Swashbuckle.Extensions;
services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("V1", new OpenApiInfo { Title = $"API", Version = "V1" });
    // Register required stuff by calling 'AddFilterExpressionsSupport()' on SwaggerGenOptions instance
    options.AddFilterExpressionsSupport();
});

Register XML Documentation

To get descriptions for generated parameters from XML documentation, paths to documentation files can be provided

services
.AddSwaggerGen(options =>
{
    options.SwaggerDoc("V1", new OpenApiInfo { Title = $"API", Version = "V1" });
    // Register required stuff including XML documentatioin files
    var filterCreatorDoc = Path.Combine(AppContext.BaseDirectory, "FS.FilterExpressionCreator.xml");
    options.AddFilterExpressionsSupport(filterCreatorDoc);
    options.IncludeXmlComments(filterCreatorDoc);
});

Support for Newtonsoft.Json

By default System.Text.Json is used to serialize/convert Filter Expression Creator specific stuff. If you like to use Newtonsoft.Json instead you must register it

Package Manager : Install-Package Schick.FilterExpressionCreator.Mvc.Newtonsoft
CLI : dotnet add package Schick.FilterExpressionCreator.Mvc.Newtonsoft
using FS.FilterExpressionCreator.Mvc.Newtonsoft;

// Register support for Newtonsoft by calling 
// 'AddFilterExpressionsNewtonsoftSupport()' on IMvcBuilder instance
services.AddControllers().AddFilterExpressionsNewtonsoftSupport();

Advanced Scenarios

Deep Copy Filters

The EntityFilter<T> class supports deep cloning by calling the Clone() method

var copy = filter.Clone();

Cast Filters

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)

var dtoFilter = new EntityFilter<OrderDto>().Add(...);
var orderFilter = dtoFilter.Cast<Order>();

Serialize Filters

Using System.Text.Json

Objects of type EntityFilter<T> can be serialized via System.Text.Json.JsonSerializer without further requirements

var json = JsonSerializer.Serialize(filter);
filter = JsonSerializer.Deserialize<EntityFilter<EntityFilter<Order>>>(json);

Using Newtonsoft.Json

When using Newtonsoft.Json additional converters are required

Package Manager : Install-Package Schick.FilterExpressionCreator.Newtonsoft
CLI : dotnet add package Schick.FilterExpressionCreator.Newtonsoft
using FS.FilterExpressionCreator.Newtonsoft.Extensions;

var json = JsonConvert.SerializeObject(filter, JsonConverterExtensions.NewtonsoftConverters);
filter = JsonConvert.DeserializeObject<EntityFilter<Order>>(json, JsonConverterExtensions.NewtonsoftConverters);

Combine Filter Expressions

To add custom checks to a filter either call .Where(...) again

var filteredOrders = orders
    .Where(filter)
    .Where(item => item.Items.Count > 2);

or where this isn't possible combine filters with CombineWithConditionalAnd

using FS.FilterExpressionCreator.Abstractions.Extensions;

var extendedFilter = new[]
    {
        filter.CreateFilter(),
        item => item.Items.Count > 2
    }
    .CombineWithConditionalAnd();

var filteredOrders = orders.Where(extendedFilter.Compile());
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
5.0.0-alpha.2 134 2/23/2023