AvaProtein.BMS.Framework.DynamicFilterBuilder
1.0.1
dotnet add package AvaProtein.BMS.Framework.DynamicFilterBuilder --version 1.0.1
NuGet\Install-Package AvaProtein.BMS.Framework.DynamicFilterBuilder -Version 1.0.1
<PackageReference Include="AvaProtein.BMS.Framework.DynamicFilterBuilder" Version="1.0.1" />
<PackageVersion Include="AvaProtein.BMS.Framework.DynamicFilterBuilder" Version="1.0.1" />
<PackageReference Include="AvaProtein.BMS.Framework.DynamicFilterBuilder" />
paket add AvaProtein.BMS.Framework.DynamicFilterBuilder --version 1.0.1
#r "nuget: AvaProtein.BMS.Framework.DynamicFilterBuilder, 1.0.1"
#:package AvaProtein.BMS.Framework.DynamicFilterBuilder@1.0.1
#addin nuget:?package=AvaProtein.BMS.Framework.DynamicFilterBuilder&version=1.0.1
#tool nuget:?package=AvaProtein.BMS.Framework.DynamicFilterBuilder&version=1.0.1
AvaProtein.BMS.Framework.DynamicFilterBuilder
A high-performance, enterprise-grade .NET library for building dynamic LINQ expressions and database queries from JSON-based filter criteria. This framework provides compile-time type safety, extensible architecture, and comprehensive validation for creating complex filtering logic that seamlessly integrates with Entity Framework Core, in-memory collections, and any LINQ-compatible data source.
๐ Key Features
- ๐ฏ Type-Safe Filtering: Compile-time validation with comprehensive operator-type compatibility checking
- ๐ง Flexible Property Mapping: Support for up to 4-level nested property mappings with fluent configuration API
- โก Performance Optimized: Efficient expression tree generation with minimal overhead
- ๐ก๏ธ Comprehensive Validation: 20+ validation rules with detailed error reporting
- ๐ JSON Integration: Seamless JSON filter parsing with case-insensitive property names
- ๐ Advanced Operations: 16 built-in filter operators including range, collection, and pattern matching
- ๐ Async/Await Support: Built-in async operations for Entity Framework Core and in-memory collections
- ๐ Pagination & Sorting: Integrated pagination and sorting with default sort configuration and optimal query execution
- ๐๏ธ Collection Filtering: Advanced collection filtering with
Any()operations for one-to-many relationships - ๐จ Extensible Architecture: Easy to extend with custom operators, validators, and property mappings
๐ฆ Installation
Install the package via NuGet Package Manager:
dotnet add package AvaProtein.BMS.Framework.DynamicFilterBuilder
Or via Package Manager Console:
Install-Package AvaProtein.BMS.Framework.DynamicFilterBuilder
More details on nuget.
๐ Quick Start
1. Define Query Options Request
Create a request model for query parameters:
public sealed record QueryOptionsRequest(
string? Filter,
string? SortProperty,
bool Descending = true,
int PageIndex = 1,
int ItemsPerPage = 50);
2. Create a Property Mapper
Define how your view model properties map to entity properties:
using Ava.BMS.Framework.DynamicFilterBuilder.Abstractions;
public class CustomerFilterMapper : PropertyMapper<CustomerListViewModel, Customer>
{
public override void Configure()
{
// Basic properties
Property(vm => vm.Id, e => e.Id);
Property(vm => vm.FirstName, e => e.FirstName);
Property(vm => vm.LastName, e => e.LastName);
Property(vm => vm.Email, e => e.Email);
Property(vm => vm.Phone, e => e.Phone);
// Enum mappings
Property(vm => vm.StatusTitle, e => e.Status.ToString());
Property(vm => vm.StatusId, e => (int)e.Status);
// Nested property mapping
Property(vm => vm.CompanyName, e => e.Company.Name);
Property(vm => vm.AddressCity, e => e.Address.City);
// Audit fields
Property(vm => vm.CreatedBy, e => e.CreatedBy.FullName);
Property(vm => vm.CreatedDate, e => e.CreateDate);
Property(vm => vm.UpdatedBy, e => e.UpdatedBy.FullName);
Property(vm => vm.UpdatedDate, e => e.UpdateDate);
// Collection property mapping
CollectionProperty(
vm => vm.HasOrderStatus,
e => e.Orders,
order => order.Status
);
// Configure default sorting (newest first)
DefaultSortConfig(e => e.CreateDate, descending: true);
}
}
3. Apply Dynamic Filtering in Service
Use the QueryOptionsFactory and extension method in your service:
using Ava.BMS.Framework.DynamicFilterBuilder.Extensions;
using Ava.BMS.Framework.DynamicFilterBuilder.Models;
public class CustomerService
{
private readonly ApplicationDbContext context;
public CustomerService(ApplicationDbContext context)
{
this.context = context;
}
public async Task<QueryResult<Customer>> GetCustomersAsync(
QueryOptionsRequest request,
CancellationToken cancellationToken,
bool asNoTracking = true)
{
// Create query options using factory
var queryOptions = QueryOptionsFactory.Create<Customer, CustomerFilterMapper>(request);
// Build base query with includes
var query = context.Customers
.Include(c => c.Company)
.Include(c => c.Address)
.Include(c => c.CreatedBy)
.Include(c => c.UpdatedBy)
.AsQueryable();
// Apply additional business logic filters if needed
query = query.Where(c => c.IsActive);
// Apply dynamic filtering, sorting, and pagination
return await query
.AsNoTracking(asNoTracking)
.ApplyQueryAsync(queryOptions, cancellationToken);
}
}
// Usage in controller
[HttpPost("search")]
public async Task<QueryResult<Customer>> SearchCustomers([FromBody] QueryOptionsRequest request)
{
var result = await customerService.GetCustomersAsync(request, cancellationToken);
Console.WriteLine($"Found {result.TotalCount} customers");
foreach (var customer in result.Entities)
{
Console.WriteLine($"{customer.FirstName} {customer.LastName}");
}
return result;
}
๐ Supported Filter Operators
The framework supports 16 comprehensive filter operators:
| Operator | Description | JSON Example | Supported Types |
|---|---|---|---|
Eq |
Equals | {"prop":"Name","op":"Eq","vl":"John"} |
All types |
Neq |
Not equals | {"prop":"Status","op":"Neq","vl":"Deleted"} |
All types |
Gt |
Greater than | {"prop":"Age","op":"Gt","vl":18} |
Numeric, DateTime, DateTimeOffset |
Lt |
Less than | {"prop":"Price","op":"Lt","vl":100.50} |
Numeric, DateTime, DateTimeOffset |
Gte |
Greater than or equal | {"prop":"Score","op":"Gte","vl":85} |
Numeric, DateTime, DateTimeOffset |
Lte |
Less than or equal | {"prop":"Budget","op":"Lte","vl":5000} |
Numeric, DateTime, DateTimeOffset |
Contains |
String contains | {"prop":"Email","op":"Contains","vl":"@company.com"} |
String only |
NotContains |
String does not contain | {"prop":"Name","op":"NotContains","vl":"Test"} |
String only |
StartsWith |
String starts with | {"prop":"Code","op":"StartsWith","vl":"PRD-"} |
String only |
EndsWith |
String ends with | {"prop":"File","op":"EndsWith","vl":".pdf"} |
String only |
Like |
SQL-like pattern matching | {"prop":"Name","op":"Like","vl":"%John%"} |
String only |
Null |
Is null | {"prop":"DeletedDate","op":"Null","vl":null} |
All nullable types |
NotNull |
Is not null | {"prop":"Email","op":"NotNull","vl":null} |
All nullable types |
Empty |
String is empty | {"prop":"Notes","op":"Empty","vl":""} |
String only |
NotEmpty |
String is not empty | {"prop":"Name","op":"NotEmpty","vl":""} |
String only |
NotNullAndNotEmpty |
String is not null and not empty | {"prop":"RequiredField","op":"NotNullAndNotEmpty","vl":""} |
String only |
In |
In collection | {"prop":"Status","op":"In","vl":["Active","Premium"]} |
All types |
Between |
Range (inclusive) | {"prop":"Age","op":"Between","vl":[18,65]} |
Numeric, DateTime, DateTimeOffset |
๐ง Default Sorting Configuration
The framework supports configuring default sorting behavior at the mapper level. When no explicit sort property is provided in the query options, the mapper's default sort configuration will be automatically applied.
public class ProductFilterMapper : PropertyMapper<ProductListViewModel, Product>
{
public override void Configure()
{
Property(vm => vm.Id, e => e.Id);
Property(vm => vm.Name, e => e.ProductName);
Property(vm => vm.Price, e => e.Price);
Property(vm => vm.CategoryName, e => e.Category.Name);
Property(vm => vm.CategoryId, e => e.Category.Id);
Property(vm => vm.IsActive, e => e.IsActive);
Property(vm => vm.StockQuantity, e => e.StockQuantity);
Property(vm => vm.CreatedDate, e => e.CreatedDate);
// Configure default sorting - products sorted by creation date (newest first)
DefaultSortConfig(e => e.CreatedDate, descending: true);
}
}
// Query without explicit sorting - will use default sort from mapper
var request = new QueryOptionsRequest(
Filter: """[{"prop":"CategoryName","op":"Eq","vl":"Electronics"}]""",
SortProperty: null, // No explicit sort - will use default
Descending: true,
PageIndex: 1,
ItemsPerPage: 10
);
var queryOptions = QueryOptionsFactory.Create<Product, ProductFilterMapper>(request);
// Query with explicit sorting - overrides default sort
var explicitSortRequest = new QueryOptionsRequest(
Filter: """[{"prop":"CategoryName","op":"Eq","vl":"Electronics"}]""",
SortProperty: "Price", // Explicit sort overrides default
Descending: false,
PageIndex: 1,
ItemsPerPage: 10
);
var explicitSortOptions = QueryOptionsFactory.Create<Product, ProductFilterMapper>(explicitSortRequest);
Default Sort Behavior
- No Sort Specified: Uses mapper's
DefaultSortConfigif configured - Explicit Sort Specified: Overrides default and uses the specified property and direction
- No Default Configured: Returns results in database natural order (no sorting applied)
๐ ๏ธ Advanced Usage Examples
Complex Multi-Condition Filtering
public async Task<QueryResult<User>> GetActiveUsersAsync(
QueryOptionsRequest request,
CancellationToken cancellationToken)
{
// Complex filter example
var complexFilter = """
[
{"prop":"Age","op":"Between","vl":[25,45]},
{"prop":"StatusId","op":"In","vl":[1,2,3]},
{"prop":"Email","op":"NotNull","vl":null},
{"prop":"LastLoginDate","op":"Gte","vl":"2024-01-01T00:00:00.000Z"},
{"prop":"CompanyName","op":"Like","vl":"%Tech%"}
]
""";
var filterRequest = request with { Filter = complexFilter };
var queryOptions = QueryOptionsFactory.Create<User, UserFilterMapper>(filterRequest);
var query = context.Users
.Include(u => u.Company)
.Include(u => u.Profile)
.Where(u => u.IsActive);
return await query.ApplyQueryAsync(queryOptions, cancellationToken);
}
Streaming data
public async IAsyncEnumerable<Orders> GetOrdersStreamAsync(
QueryOptionsRequest request,
CancellationToken cancellationToken)
{
var complexFilter = """
[
{"prop":"Amount","op":"Between","vl":[1400,1450]}
]
""";
var filterRequest = request with { Filter = complexFilter };
var queryOptions = QueryOptionsFactory.Create<Order, OrderFilterMapper>(filterRequest);
var query = context.Orders.Where(u => u.IsActive);
await foreach (var order in query.ApplyQueryAsStream(queryOptions, cancellationToken))
yield return order;
}
Nested Property Filtering with Default Sorting
public class OrderFilterMapper : PropertyMapper<OrderListViewModel, Order>
{
public override void Configure()
{
Property(vm => vm.Id, e => e.Id);
Property(vm => vm.OrderNumber, e => e.OrderNumber);
Property(vm => vm.CustomerName, e => e.Customer.FullName);
Property(vm => vm.CustomerId, e => e.Customer.Id);
Property(vm => vm.ShippingCity, e => e.ShippingAddress.City);
Property(vm => vm.ShippingCountry, e => e.ShippingAddress.Country.Name);
Property(vm => vm.CountryCode, e => e.ShippingAddress.Country.Code);
Property(vm => vm.TotalAmount, e => e.TotalAmount);
Property(vm => vm.StatusTitle, e => e.Status.ToString());
Property(vm => vm.StatusId, e => (int)e.Status);
Property(vm => vm.OrderDate, e => e.OrderDate);
Property(vm => vm.CreatedBy, e => e.CreatedBy.FullName);
// Set default sorting by order date (newest first)
DefaultSortConfig(e => e.OrderDate, descending: true);
}
}
public async Task<QueryResult<Order>> GetOrdersByCountryAsync(
string countryCode,
QueryOptionsRequest request,
CancellationToken cancellationToken)
{
// Add country filter to existing request
var countryFilter = $"""[{{"prop":"CountryCode","op":"Eq","vl":"{countryCode}"}}]""";
var filterRequest = request with { Filter = countryFilter };
var queryOptions = QueryOptionsFactory.Create<Order, OrderFilterMapper>(filterRequest);
var query = context.Orders
.Include(o => o.Customer)
.Include(o => o.ShippingAddress)
.ThenInclude(a => a.Country)
.Include(o => o.CreatedBy);
return await query.ApplyQueryAsync(queryOptions, cancellationToken);
}
Collection Filtering
public class CustomerFilterMapper : PropertyMapper<CustomerListViewModel, Customer>
{
public override void Configure()
{
Property(vm => vm.Id, e => e.Id);
Property(vm => vm.FirstName, e => e.FirstName);
Property(vm => vm.LastName, e => e.LastName);
Property(vm => vm.Email, e => e.Email);
Property(vm => vm.CompanyName, e => e.Company.Name);
// Filter customers who have orders with specific status
CollectionProperty(
vm => vm.HasOrderStatus,
e => e.Orders,
order => order.Status
);
// Filter customers who have ordered specific products
CollectionProperty(
vm => vm.HasProductId,
e => e.Orders.SelectMany(o => o.Items),
item => item.ProductId
);
// Filter customers by order date ranges
CollectionProperty(
vm => vm.HasRecentOrder,
e => e.Orders,
order => order.OrderDate
);
// Configure default sorting by customer name
DefaultSortConfig(e => e.LastName, descending: false);
}
}
public async Task<QueryResult<Customer>> GetCustomersWithPendingOrdersAsync(
QueryOptionsRequest request,
CancellationToken cancellationToken)
{
var pendingOrdersFilter = """[{"prop":"HasOrderStatus","op":"Eq","vl":"Pending"}]""";
var filterRequest = request with { Filter = pendingOrdersFilter };
var queryOptions = QueryOptionsFactory.Create<Customer, CustomerFilterMapper>(filterRequest);
var query = context.Customers
.Include(c => c.Company)
.Include(c => c.Orders)
.ThenInclude(o => o.Items);
return await query.ApplyQueryAsync(queryOptions, cancellationToken);
}
// Usage with multiple product IDs
var productFilter = """[{"prop":"HasProductId","op":"In","vl":[123,456,789]}]""";
Manual Filter Building
using Ava.BMS.Framework.DynamicFilterBuilder.Models;
using Ava.BMS.Framework.DynamicFilterBuilder.Factories;
public async Task<List<Employee>> GetEmployeesByCustomCriteriaAsync(
string namePrefix,
int minAge,
int maxAge,
List<int> departmentIds,
CancellationToken cancellationToken)
{
// Create filters programmatically
var filters = new List<FilterDescriptor>
{
new() {
PropertyName = "FirstName",
Operator = FilterOperator.StartsWith,
Value = namePrefix
},
new() {
PropertyName = "Age",
Operator = FilterOperator.Between,
Value = new[] { minAge, maxAge }
},
new() {
PropertyName = "DepartmentId",
Operator = FilterOperator.In,
Value = departmentIds
},
new() {
PropertyName = "IsActive",
Operator = FilterOperator.Eq,
Value = true
}
};
// Build predicate directly using mapper
var mapper = new EmployeeFilterMapper();
var predicate = QueryBuilder.BuildPredicate(filters, mapper);
// Apply to queryable with additional business logic
var query = context.Employees
.Include(e => e.Department)
.Include(e => e.Manager)
.Where(predicate)
.Where(e => e.HireDate <= DateTime.Now); // Additional business rule
return await query.ToListAsync(cancellationToken);
}
๐๏ธ Architecture Overview
Core Components
QueryOptionsRequest: Record for incoming query parameters from API requestsQueryOptionsFactory: Factory for creating strongly-typed QueryOptions from requestsQueryOptions<TEntity>: Configuration class for query parameters including filters, sorting, and paginationPropertyMapper<TSource, TDestination>: Abstract base class for defining property mappings with fluent API and default sort configurationQueryableExtensions: Extension methods providing the mainApplyQueryAsyncentry pointFilterDescriptor: Represents individual filter conditions with property, operator, and valueQueryBuilder: Core engine for building LINQ expressions from filter specificationsExpressionBuilder: Specialized builder for generating type-safe LINQ expressions for each operatorFilterValidator: Comprehensive validation engine with type compatibility checkingFilterParser: JSON parsing engine with support for complex data types
Expression Tree Generation
The framework uses sophisticated expression tree building techniques to generate optimal LINQ queries:
// Generated expression tree for complex filter
entity => entity.Age >= 25 &&
entity.Age <= 65 &&
new[] { "Active", "Premium" }.Contains(entity.Status) &&
entity.Orders.Any(o => o.Status == "Pending")
Typical use cases
- Building advanced search endpoints for CRM or ERP systems.
- Filtering large datasets in data warehouses.
- Supporting dynamic reporting in multi-tenant SaaS platforms
๐ Validation & Error Handling
The framework provides comprehensive validation with detailed error messages:
try
{
var result = await query.ApplyQueryAsync(queryOptions, cancellationToken);
}
catch (FilterBuilderException ex)
{
// Property mapping errors
logger.LogError("Mapping error: {Message}", ex.Message);
}
catch (InvalidFilterConfigurationException ex)
{
// Filter validation errors
logger.LogError("Validation error: {Message}", ex.Message);
}
catch (UnsupportedOperatorException ex)
{
// Unsupported operator errors
logger.LogError("Operator error: {Message}", ex.Message);
}
Validation Rules
- Type Compatibility: Ensures operators are compatible with property types
- Value Validation: Validates values for specific operators (e.g., Between requires exactly 2 values)
- Collection Validation: Ensures In operators have at least 1 value
- Range Limits: Maximum 20 filters per query, maximum 200 characters per filter value
- Property Existence: Validates that all referenced properties exist in mappings
โก Performance Considerations
Best Practices
- Property Mapping Reuse: Create mapper instances once and reuse across multiple queries
- Expression Caching: Consider caching generated expressions for frequently used filter combinations
- Database Indexing: Ensure database indexes align with commonly filtered properties
- Pagination: Always use pagination for large datasets (max 10,000 items per page)
- Async Operations: Use async methods for Entity Framework Core operations
Performance Metrics
- Expression Generation: < 1ms for typical filter combinations
- Memory Allocation: Minimal allocations through expression tree reuse
- Query Optimization: Generated LINQ expressions are fully compatible with Entity Framework Core query optimization
๐งช Testing
The framework includes comprehensive test coverage with 400+ test cases covering:
- All 16 filter operators with various data types
- Property mapping scenarios (simple, nested, collection)
- Validation rules and error conditions
- Async operations and cancellation
- Edge cases and boundary conditions
- Performance scenarios with large datasets
๐ค Contributing
We welcome contributions! Please read our contributing guidelines and submit pull requests to our repository.
Development Setup
- Clone the repository
- Install .NET 9.0 SDK
- Run tests:
dotnet test - Build package:
dotnet pack
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Support
- Documentation: Wiki
- Issues: Issue Tracker
- Discussions: GitHub Discussions
๐ข About AvaProtein
Built with โค๏ธ by the AvaProtein team as part of the AVA Business Management System framework.
change logs:
- 11.03.2025 - Updated
Microsoft.Extensions.Logging.Abstractionstov9.0.10 - 11.03.2025 - Updated
Microsoft.EntityFrameworkCoretov9.0.10 - 11.10.2025 - Adding
ApplyQueryAsStreamto stream data throughIAsyncEnumerable. - 11.15.2025 - Updated
.NET10and all dependencies - 11.15.2025 - Adding
CountAsynctoQueryableExtensionsto count anEntity<T>usingQueryOptions<T>
Keywords: Dynamic Filtering, LINQ, Entity Framework Core, .NET 9, Expression Trees, JSON Filtering, Type Safety, Enterprise Library, Query Builder, ORM
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
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.0.1 | 125 | 2/14/2026 | |
| 1.0.0 | 170 | 1/5/2026 | |
| 0.0.2-alpha.7 | 219 | 11/15/2025 | |
| 0.0.2-alpha.6 | 190 | 11/15/2025 | |
| 0.0.2-alpha.5 | 239 | 11/10/2025 | |
| 0.0.2-alpha.4 | 188 | 11/3/2025 | |
| 0.0.2-alpha.3 | 198 | 10/24/2025 | |
| 0.0.2-alpha.2 | 208 | 9/2/2025 | |
| 0.0.2-alpha.1 | 176 | 9/2/2025 | |
| 0.0.1-alpha9 | 395 | 8/31/2025 | |
| 0.0.1-alpha17 | 386 | 9/2/2025 | |
| 0.0.1-alpha16 | 375 | 9/2/2025 | |
| 0.0.1-alpha11 | 382 | 8/31/2025 | |
| 0.0.1-alpha.19 | 118 | 10/24/2025 | |
| 0.0.1-alpha.18 | 353 | 9/2/2025 |
v1.0.0:
- Initial release
- Support for 16 filter operators (Eq, Neq, Gt, Lt, Gte, Lte, Contains, Like, etc.)
- Type-safe property mapping with up to 4-level nested properties
- Collection filtering with Any() operations
- Comprehensive validation with Persian localization
- JSON filter parsing and serialization
- Async query support for EF Core and in-memory collections
- Extensive test coverage (400+ test cases)