GenericFilters 1.1.0

dotnet add package GenericFilters --version 1.1.0
                    
NuGet\Install-Package GenericFilters -Version 1.1.0
                    
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="GenericFilters" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GenericFilters" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="GenericFilters" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GenericFilters --version 1.1.0
                    
#r "nuget: GenericFilters, 1.1.0"
                    
#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.
#:package GenericFilters@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GenericFilters&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=GenericFilters&version=1.1.0
                    
Install as a Cake Tool

๐Ÿ” GenericFilters NuGet

A powerful and extensible filtering framework for C# applications. GenericFilters enables dynamic, attribute-driven filtering logic for LINQ queries, in-memory collections, and Cosmos DB SDK queries.


๐Ÿ”ง Installation

.NET CLI

dotnet add package GenericFilters

Package Manager

Install-Package GenericFilters


๐Ÿ“ฆ Features

  • โœ… Attribute-based filtering with FilterMemberAttribute
  • ๐Ÿ”„ Supports string, list, DateTime and numeric comparisons
  • ๐Ÿง  Logical operations (AND / OR) between filters
  • ๐Ÿงฐ Customizable filter behavior with FilterOptions
  • ๐Ÿงช Built-in validation and error handling
  • โš™๏ธ Expression tree generation for LINQ queries

๐Ÿ“ Components

1. Filter

An abstract base class that:

  • Validates filter properties at runtime
  • Generates LINQ expressions dynamically
  • Supports pagination (StartingIndex, PageSize)
  • Provides utility methods: Any(), All(), GetQueryExpression()

2. FilterMemberAttribute

Decorates filter properties to define:

  • Target model property (Name)
  • String comparison method (Equals, Contains)
  • Case sensitivity
  • Dates and numbers comparison operations
  • Logical grouping (And, Or)
  • Inclusion/exclusion in query generation

3. FilterOptions

Controls runtime behavior:

  • Optimistic: If true, ignores missing model properties instead of throwing exceptions

๐Ÿš€ Getting Started

1. Define a Model

public record Product(string Name, List<string> Tags, 
    double UnitPrice, DateTime? CreatedAt);

2. Define a Filter

using GenericFilters;

public class ProductFilter : Filter<Product>
{
    [FilterMember]
    public string Name { get; init; }

    [FilterMember(stringComparisonMethod: StringComparisonMethod.Contains, stringComparisonIgnoreCase: true)]
    public List<string> Tags { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public double? PriceFrom { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.LessThan)]
    public double? PriceTo { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public DateTime? StartDate { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.LessThanOrEqual)]
    public DateTime? EndDate { get; init; }
}

3. Apply the Filter

using GenericFilters.Extensions;

var products = new List<Product>
{
    new Product("book", [ "education", "programming", "software" ], 
        58.90, new DateTime(2025, 2, 12)),
    new Product("phone", [ "android", "electronics" ],
        770.00, new DateTime(2023, 6, 8)),
    new Product("laptop", [ "linux", "electronics", "entertaiment" ], 
        3999.99, new DateTime(2024, 7, 22)),
};

var filter = new ProductFilter
{
    Name = "book", 
    Tags = [ "education", "engineering" ], 
    PriceFrom = 40.00,
    PriceTo = 100.00,
    StartDate = new DateTime(2025, 2, 1), 
    EndDate = new DateTime(2025, 3, 1)
};

var filteredProducts = products.FilterBy(filter).ToList();

3.1. Same scenario using GetQueryExpression() method

var expression = filter.GetQueryExpression();

var filteredProducts = products.AsQueryable()
    .Where(expression)
    .ToList();

โ–ถ๏ธ Run this code on .NET Fiddle


โš™๏ธ Advanced Options

Optimistic Filtering

var options = new FilterOptions { Optimistic = true };
var expression = filter.GetQueryExpression(options);

This allows filters to skip missing model properties without throwing exceptions.

IgnoreInQueryExpression

When IgnoreInQueryExpression is set to true in the FilterMember attribute, that property is skipped when we build Filter expression

[FilterMember(ignoreInQueryExpression: true)]
public List<string> Items { get; init; }

You can use that parameter in cases when you need to implement any custom filtering logic. More details about custom filtering is provided under section related to GetQueryExpressionExt method

It is the same when we don't provide any attribute for that property at all, but the difference is, If we provide FilterMember, that property will be taken into consideration when we call getHashCode(), Any() or All() methods of the Filter class.

Nested properties

Starting from ver. 1.1.0 GenericFilters supports nested properties using dot '.' notation.

1. Define a Model

public class Product
{
    public string Name { get; init; }
    public List<string> Tags { get; init; }
    public double UnitPrice { get; init; } 
    public List<ProductItem> Items { get; init; }
    public DateTime CreatedAt { get; init; }
};

public record ProductItem (string Sku, int Quantity);

2. Define a Filter

using System.Linq.Expressions;
using GenericFilters;
using LinqKit;

public class ProductFilter : Filter<Product>
{
    [FilterMember]
    public string Name { get; init; }

    [FilterMember(stringComparisonMethod: StringComparisonMethod.Contains, stringComparisonIgnoreCase: true)]
    public List<string> Tags { get; init; }
    
    [FilterMember("Items.Sku")]
    public List<string> Items { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public double? PriceFrom { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.LessThan)]
    public double? PriceTo { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public DateTime? StartDate { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.LessThanOrEqual)]
    public DateTime? EndDate { get; init; }
}

3. Apply the Filter

using GenericFilters.Extensions;

var products = new List<Product>
{
    new Product
    {
        Name = "External Storage Bundle",
        Tags = new List<string> { "storage", "bundle", "external" },
        UnitPrice = 129.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-1001", 25),
            new ProductItem("USB-64GB", 100),
            new ProductItem("SD-128GB", 50)
        }
    },
    new Product
    {
        Name = "Portable Backup Kit",
        Tags = new List<string> { "backup", "portable", "data", "storage" },
        UnitPrice = 89.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-1001", 20),
            new ProductItem("CASE-01", 30)
        }
    },
    new Product
    {
        Name = "Hard Drive",
        Tags = new List<string> { "storage", "hard drive" },
        UnitPrice = 59.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-2002", 40)
        }
    }
};

var filter = new ProductFilter
{
    Tags = [ "data", "storage" ], 
    Items = [ "HD-1001" ],
    PriceFrom = 80.00,
    PriceTo = 150.00,
    StartDate = new DateTime(2025, 1, 1), 
};

var filteredProducts = products.AsQueryable()
    .FilterBy(filter)
    .ToList();

โ–ถ๏ธ Run this code on .NET Fiddle

GetQueryExpressionExt method

In some cases in our model or our filter we may have some complex properties not handled by the Filter out of the box.
Or probably we need to provide some specific logic with extra conditions.

In that case we can apply the following approach:

  • Apply FilterMember attributes for all strings, numeric etc. properties as usually.
  • Mark all properties with custom logic we are going to provide as IgnoreInQueryExpression
  • Override GetQueryExpressionExt and add all custom logic using LinqKit or build Linq Expression in any another way.

When we call either GetQueryExpression or FilterBy, that custom Linq Expression will be added to the end of Expression generated for our 'standard' filter behind the scene.

Here is an example of using that approach to filter by ProductItem type property:

1. Define a Model

public class Product
{
    public string Name { get; init; }
    public List<string> Tags { get; init; }
    public double UnitPrice { get; init; } 
    public List<ProductItem> Items { get; init; }
    public DateTime CreatedAt { get; init; }
};

public record ProductItem (string Sku, int Quantity);

2. Define a Filter

using System.Linq.Expressions;
using GenericFilters;
using LinqKit;

public class ProductFilter : Filter<Product>
{
    [FilterMember]
    public string Name { get; init; }

    [FilterMember(stringComparisonMethod: StringComparisonMethod.Contains, stringComparisonIgnoreCase: true)]
    public List<string> Tags { get; init; }
    
    [FilterMember(ignoreInQueryExpression: true)]
    public List<string> Items { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public double? PriceFrom { get; init; }
    
    [FilterMember("UnitPrice", comparisonOperation: ComparisonOperation.LessThan)]
    public double? PriceTo { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.GreaterThanOrEqual)]
    public DateTime? StartDate { get; init; }

    [FilterMember("CreatedAt", comparisonOperation: ComparisonOperation.LessThanOrEqual)]
    public DateTime? EndDate { get; init; }
    
    protected override Expression<Func<Product, bool>> GetQueryExpressionExt(FilterOptions filterOptions)
    {
        var predicate = PredicateBuilder.New<Product>();
        
        // Build custom behaviour for Items using LinqKit
        predicate.And(i => Items
            .Any(t => i.Items.Any(i => i.Sku == t && i.Quantity > 0)));
        
        return predicate;
    }
}

3. Apply the Filter

using GenericFilters.Extensions;

var products = new List<Product>
{
    new Product
    {
        Name = "External Storage Bundle",
        Tags = new List<string> { "storage", "bundle", "external" },
        UnitPrice = 129.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-1001", 25),
            new ProductItem("USB-64GB", 100),
            new ProductItem("SD-128GB", 50)
        }
    },
    new Product
    {
        Name = "Portable Backup Kit",
        Tags = new List<string> { "backup", "portable", "data", "storage" },
        UnitPrice = 89.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-1001", 20),
            new ProductItem("CASE-01", 30)
        }
    },
    new Product
    {
        Name = "Hard Drive",
        Tags = new List<string> { "storage", "hard drive" },
        UnitPrice = 59.99,
        CreatedAt = new DateTime(2025, 2, 1),
        Items = new List<ProductItem>
        {
            new ProductItem("HD-2002", 40)
        }
    }
};

var filter = new ProductFilter
{
    Tags = [ "data", "storage" ], 
    Items = ["HD-1001" ],
    PriceFrom = 80.00,
    PriceTo = 150.00,
    StartDate = new DateTime(2025, 1, 1), 
};

var filteredProducts = products.AsQueryable()
    .FilterBy(filter)
    .ToList();

โ–ถ๏ธ Run this code on .NET Fiddle

LogicalOperation

FilterMember attribute supports LogicalOperation parameter. By default, all properties are selected using And logic. There is also Or option available. But we should be careful in case of mixing And and Or together. Since there is no grouping option available, it depends on attributes order, so we need to take that order and logical operations priority into account. Otherwise, we may end-up with non-deterministic result.

public record TestModel(string Prop1, string Prop2, string Prop3, string Prop4);

public class TestFilter : Filter<TestModel>
{
    [FilterMember]
    public string Prop1 { get; init; }
    
    [FilterMember(logicalOperation: LogicalOperation.Or)]
    public string Prop2 { get; init; }
    
    [FilterMember]
    public string Prop3 { get; init; }
    
    [FilterMember(logicalOperation: LogicalOperation.Or)]
    public string Prop4 { get; init; }
}

var model = new List<TestModel> { new TestModel("a", "b", "c", "d") };
var filter = new TestFilter
{
    Prop1 = "a",
    Prop2 = "b",
    Prop3 = "c",
    Prop4 = "z",
};

var query = filter.GetQueryExpression();
var result = model.AsQueryable().Where(query).ToList();

Console.WriteLine(query.ToString());
Console.WriteLine(result.Count);

Result:<br> <i> Param_0 โ‡’ (((Param_0.Prop1.Equals("a") OrElse Param_0.Prop2.Equals("b")) AndAlso Param_0.Prop3.Equals("c")) OrElse Param_0.Prop4.Equals("z")) <br> 1 </i>

โ–ถ๏ธ Run this code on .NET Fiddle

In order to avoid such sort of issues, it is recommended approach to provide our custom logic in the GetQueryExpressionExt method.

Pagination support

There are 2 properties provided in the Filter class in order to support pagination

public int StartIndex { get; set; } = -1;
public int PageSize { get; set; } = -1;

Those properties can be set in the Filter and tracked e.g. on UI side. When we call FilterBy, internally it will be added Skip() and Take() to our expression. If we are using GetQueryExpression, we need to build that logic by ourselves

var filteredProducts = products.AsQueryable()
    .Where(filter.GetQueryExpression())
    .Take(filter.StartIndex).Skip(filter.PageSize);

๐Ÿงช Validation & Safety

  • Only supports string, List<string>, DateTime, double, decimal and int filter types, with nullables
  • Throws FilterException for unsupported types or misconfigurations
  • Ensures at least one valid filter is defined

๐Ÿ“Œ Notes

  • Not all features are compatible with IQueryable in Entity Framework
  • Case-insensitive filtering may impact performance if DB collation is not case-sensitive
  • You can override GetQueryExpressionExt() for custom logic

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.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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
1.1.0 122 8/17/2025
1.0.0 494 7/23/2025
1.0.0-alpha.4 26 7/19/2025
1.0.0-alpha.3 132 6/29/2025
1.0.0-alpha.2 119 6/22/2025
1.0.0-alpha.1 270 6/11/2025