GenericFilters 1.1.0
dotnet add package GenericFilters --version 1.1.0
NuGet\Install-Package GenericFilters -Version 1.1.0
<PackageReference Include="GenericFilters" Version="1.1.0" />
<PackageVersion Include="GenericFilters" Version="1.1.0" />
<PackageReference Include="GenericFilters" />
paket add GenericFilters --version 1.1.0
#r "nuget: GenericFilters, 1.1.0"
#:package GenericFilters@1.1.0
#addin nuget:?package=GenericFilters&version=1.1.0
#tool nuget:?package=GenericFilters&version=1.1.0
๐ GenericFilters 
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
: Iftrue
, 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
andint
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 | Versions 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. |
-
.NETStandard 2.0
- LinqKit.Core (>= 1.2.8)
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 |