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
                    
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="AvaProtein.BMS.Framework.DynamicFilterBuilder" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AvaProtein.BMS.Framework.DynamicFilterBuilder" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="AvaProtein.BMS.Framework.DynamicFilterBuilder" />
                    
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 AvaProtein.BMS.Framework.DynamicFilterBuilder --version 1.0.1
                    
#r "nuget: AvaProtein.BMS.Framework.DynamicFilterBuilder, 1.0.1"
                    
#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 AvaProtein.BMS.Framework.DynamicFilterBuilder@1.0.1
                    
#: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=AvaProtein.BMS.Framework.DynamicFilterBuilder&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=AvaProtein.BMS.Framework.DynamicFilterBuilder&version=1.0.1
                    
Install as a Cake Tool

AvaProtein.BMS.Framework.DynamicFilterBuilder

NuGet .NET License: MIT

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

  1. No Sort Specified: Uses mapper's DefaultSortConfig if configured
  2. Explicit Sort Specified: Overrides default and uses the specified property and direction
  3. 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 requests
  • QueryOptionsFactory: Factory for creating strongly-typed QueryOptions from requests
  • QueryOptions<TEntity>: Configuration class for query parameters including filters, sorting, and pagination
  • PropertyMapper<TSource, TDestination>: Abstract base class for defining property mappings with fluent API and default sort configuration
  • QueryableExtensions: Extension methods providing the main ApplyQueryAsync entry point
  • FilterDescriptor: Represents individual filter conditions with property, operator, and value
  • QueryBuilder: Core engine for building LINQ expressions from filter specifications
  • ExpressionBuilder: Specialized builder for generating type-safe LINQ expressions for each operator
  • FilterValidator: Comprehensive validation engine with type compatibility checking
  • FilterParser: 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

  1. Property Mapping Reuse: Create mapper instances once and reuse across multiple queries
  2. Expression Caching: Consider caching generated expressions for frequently used filter combinations
  3. Database Indexing: Ensure database indexes align with commonly filtered properties
  4. Pagination: Always use pagination for large datasets (max 10,000 items per page)
  5. 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

  1. Clone the repository
  2. Install .NET 9.0 SDK
  3. Run tests: dotnet test
  4. Build package: dotnet pack

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ†˜ Support

๐Ÿข 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.Abstractions to v9.0.10
  • 11.03.2025 - Updated Microsoft.EntityFrameworkCore to v9.0.10
  • 11.10.2025 - Adding ApplyQueryAsStream to stream data through IAsyncEnumerable.
  • 11.15.2025 - Updated .NET10 and all dependencies
  • 11.15.2025 - Adding CountAsync to QueryableExtensions to count an Entity<T> using QueryOptions<T>

Keywords: Dynamic Filtering, LINQ, Entity Framework Core, .NET 9, Expression Trees, JSON Filtering, Type Safety, Enterprise Library, Query Builder, ORM

Product 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. 
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.

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)