FilterEngine.Core
1.0.0
dotnet add package FilterEngine.Core --version 1.0.0
NuGet\Install-Package FilterEngine.Core -Version 1.0.0
<PackageReference Include="FilterEngine.Core" Version="1.0.0" />
<PackageVersion Include="FilterEngine.Core" Version="1.0.0" />
<PackageReference Include="FilterEngine.Core" />
paket add FilterEngine.Core --version 1.0.0
#r "nuget: FilterEngine.Core, 1.0.0"
#:package FilterEngine.Core@1.0.0
#addin nuget:?package=FilterEngine.Core&version=1.0.0
#tool nuget:?package=FilterEngine.Core&version=1.0.0
FilterEngine.Core
FilterEngine.Core is a powerful and flexible dynamic filtering engine for .NET applications. It enables you to perform runtime filtering, sorting, and pagination operations on IQueryable collections.
🚀 Features
- Dynamic Filtering: 13 different filtering operators
- Flexible Sorting: Ascending/descending sort support
- Pagination: Performant page-based data retrieval
- Type Safety: Compile-time type safety
- Entity Framework Compatible: Works seamlessly with EF Core
- Performance Focused: Fast queries based on expression trees
- Easy to Use: Simple API with extension methods
📦 Installation
Package Manager Console
Install-Package FilterEngine.Core
.NET CLI
dotnet add package FilterEngine.Core
PackageReference
<PackageReference Include="FilterEngine.Core" Version="1.0.0" />
🔧 Quick Start
1. Import Namespace
using FilterEngine.Core.Extensions;
using FilterEngine.Core.Models;
2. Simple Filtering
var products = dbContext.Products.AsQueryable();
var filterRequest = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Name", Operator = FilterOperator.Contains, Value = "laptop" },
new() { Field = "Price", Operator = FilterOperator.GreaterThan, Value = "500" }
}
};
var filteredProducts = products.ApplyFiltering(filterRequest);
3. Filtering + Pagination
var filterRequest = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Category", Operator = FilterOperator.Equals, Value = "Electronics" }
},
SortField = "Price",
SortDirection = SortDirection.Desc,
Page = 1,
PageSize = 10
};
var result = products.ApplyFilteringWithPaging(filterRequest);
Console.WriteLine($"Total: {result.TotalCount}");
Console.WriteLine($"Page: {result.Page}/{result.TotalPages}");
foreach (var product in result.Data)
{
Console.WriteLine($"{product.Name} - ${product.Price}");
}
📖 Detailed Usage
Filter Operators
| Operator | Description | Example |
|---|---|---|
Equals |
Equals | { Field = "Status", Operator = Equals, Value = "Active" } |
NotEquals |
Not equals | { Field = "Status", Operator = NotEquals, Value = "Deleted" } |
GreaterThan |
Greater than | { Field = "Price", Operator = GreaterThan, Value = "100" } |
GreaterThanOrEqual |
Greater than or equal | { Field = "Age", Operator = GreaterThanOrEqual, Value = "18" } |
LessThan |
Less than | { Field = "Stock", Operator = LessThan, Value = "10" } |
LessThanOrEqual |
Less than or equal | { Field = "Discount", Operator = LessThanOrEqual, Value = "50" } |
Contains |
Contains (String) | { Field = "Description", Operator = Contains, Value = "phone" } |
StartsWith |
Starts with (String) | { Field = "Code", Operator = StartsWith, Value = "PRD" } |
EndsWith |
Ends with (String) | { Field = "Email", Operator = EndsWith, Value = "@gmail.com" } |
IsNull |
Is null | { Field = "DeletedDate", Operator = IsNull, Value = null } |
IsNotNull |
Is not null | { Field = "CreatedBy", Operator = IsNotNull, Value = null } |
In |
In list | { Field = "CategoryId", Operator = In, Value = "1,2,3" } |
Between |
Between range | { Field = "CreatedDate", Operator = Between, Value = "2023-01-01,2023-12-31" } |
Advanced Examples
Multiple Filters
var complexFilter = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Category", Operator = FilterOperator.In, Value = "Electronics,Books,Clothing" },
new() { Field = "Price", Operator = FilterOperator.Between, Value = "50,500" },
new() { Field = "Name", Operator = FilterOperator.Contains, Value = "pro" },
new() { Field = "IsActive", Operator = FilterOperator.Equals, Value = "true" },
new() { Field = "DeletedDate", Operator = FilterOperator.IsNull, Value = null }
},
SortField = "CreatedDate",
SortDirection = SortDirection.Desc,
Page = 2,
PageSize = 20
};
var result = products.ApplyFilteringWithPaging(complexFilter);
Filtering with Enum Values
public enum ProductStatus { Active, Inactive, Discontinued }
var enumFilter = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Status", Operator = FilterOperator.Equals, Value = "Active" },
new() { Field = "Status", Operator = FilterOperator.In, Value = "Active,Inactive" }
}
};
DateTime Filtering
var dateFilter = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "CreatedDate", Operator = FilterOperator.GreaterThan, Value = "2023-01-01" },
new() { Field = "UpdatedDate", Operator = FilterOperator.Between, Value = "2023-06-01,2023-12-31" }
}
};
🏗️ Web API Integration
Controller Example
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
[HttpPost("search")]
public async Task<ActionResult<FilterResult<ProductDto>>> SearchProducts(
[FromBody] FilterRequest request)
{
try
{
var result = await _productService.GetFilteredProducts(request);
return Ok(result);
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
Service Implementation
public class ProductService : IProductService
{
private readonly AppDbContext _context;
public async Task<FilterResult<ProductDto>> GetFilteredProducts(FilterRequest request)
{
var query = _context.Products
.Where(p => !p.IsDeleted)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Category = p.Category.Name
});
return query.ApplyFilteringWithPaging(request);
}
}
🔍 Frontend Integration
JavaScript/TypeScript
const searchProducts = async (filters) => {
const filterRequest = {
filters: [
{ field: "Name", operator: "Contains", value: searchTerm },
{ field: "CategoryId", operator: "Equals", value: selectedCategory }
],
sortField: "Name",
sortDirection: "Asc",
page: currentPage,
pageSize: 20
};
const response = await fetch('/api/products/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(filterRequest)
});
const result = await response.json();
return result;
};
React Hook Example
const useProductSearch = () => {
const [filters, setFilters] = useState([]);
const [page, setPage] = useState(1);
const [sortField, setSortField] = useState('Name');
const searchProducts = useCallback(async () => {
const request = {
filters,
sortField,
sortDirection: 'Asc',
page,
pageSize: 20
};
const result = await productApi.search(request);
return result;
}, [filters, page, sortField]);
return { searchProducts, setFilters, setPage, setSortField };
};
⚡ Performance Tips
1. Use Indexes
// Create indexes for fields you'll filter on
[Index(nameof(Category))]
[Index(nameof(Price))]
[Index(nameof(CreatedDate))]
public class Product
{
// ...
}
2. Use Projections
// Select only the fields you need
var query = context.Products
.Select(p => new ProductSummary
{
Id = p.Id,
Name = p.Name,
Price = p.Price
});
var result = query.ApplyFilteringWithPaging(request);
3. Async Operations
// Use async for large datasets
public async Task<FilterResult<T>> GetFilteredDataAsync<T>(
IQueryable<T> query,
FilterRequest request)
{
var filteredQuery = query.ApplyFiltering(request);
var totalCount = await filteredQuery.CountAsync();
var data = await filteredQuery
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
return new FilterResult<T>
{
Data = data,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize
};
}
🚨 Error Handling
Common Errors and Solutions
1. Property Not Found
// ❌ Wrong
new Filter { Field = "ProductName", ... } // Property doesn't exist
// ✅ Correct
new Filter { Field = "Name", ... } // Correct property name
2. Type Conversion Error
// ❌ Wrong
new Filter { Field = "Price", Operator = FilterOperator.Equals, Value = "abc" }
// ✅ Correct
new Filter { Field = "Price", Operator = FilterOperator.Equals, Value = "99.99" }
3. Between Operator Usage
// ❌ Wrong
new Filter { Field = "Price", Operator = FilterOperator.Between, Value = "100" }
// ✅ Correct
new Filter { Field = "Price", Operator = FilterOperator.Between, Value = "100,500" }
Exception Handling
try
{
var result = products.ApplyFilteringWithPaging(request);
return Ok(result);
}
catch (ArgumentException ex)
{
// Field not found or invalid value
return BadRequest($"Filter error: {ex.Message}");
}
catch (InvalidOperationException ex)
{
// Operator error
return BadRequest($"Operation error: {ex.Message}");
}
catch (Exception ex)
{
// Unexpected error
_logger.LogError(ex, "Unexpected error in filtering");
return StatusCode(500, "Internal server error");
}
📊 Testing Examples
Unit Test
[Test]
public void ApplyFiltering_WithContainsFilter_ShouldReturnMatchingResults()
{
// Arrange
var products = new List<Product>
{
new() { Name = "iPhone 14 Pro", Category = "Electronics" },
new() { Name = "Samsung Galaxy", Category = "Electronics" },
new() { Name = "Programming Book", Category = "Books" }
}.AsQueryable();
var request = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Name", Operator = FilterOperator.Contains, Value = "phone" }
}
};
// Act
var result = products.ApplyFilteringWithPaging(request);
// Assert
Assert.AreEqual(1, result.TotalCount);
Assert.IsTrue(result.Data.Any(p => p.Name.Contains("iPhone")));
}
Integration Test
[Test]
public async Task SearchProducts_WithValidFilter_ShouldReturnResults()
{
// Arrange
var client = _factory.CreateClient();
var filterRequest = new FilterRequest
{
Filters = new List<Filter>
{
new() { Field = "Category", Operator = FilterOperator.Equals, Value = "Electronics" }
},
Page = 1,
PageSize = 10
};
// Act
var response = await client.PostAsJsonAsync("/api/products/search", filterRequest);
var result = await response.Content.ReadFromJsonAsync<FilterResult<ProductDto>>();
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
result.Should().NotBeNull();
result.Data.Should().NotBeEmpty();
result.Data.Should().OnlyContain(p => p.Category == "Electronics");
}
🔒 Security Considerations
Input Validation
public class FilterRequestValidator : AbstractValidator<FilterRequest>
{
private readonly string[] _allowedFields = { "Name", "Price", "Category", "CreatedDate" };
public FilterRequestValidator()
{
RuleFor(x => x.Page).GreaterThan(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
RuleForEach(x => x.Filters).ChildRules(filter =>
{
filter.RuleFor(f => f.Field)
.Must(field => _allowedFields.Contains(field))
.WithMessage("Invalid field name");
filter.RuleFor(f => f.Value)
.MaximumLength(1000)
.WithMessage("Filter value too long");
});
}
}
SQL Injection Protection
The library uses Expression Trees and parameterized queries, providing built-in protection against SQL injection attacks.
🌍 Internationalization
Custom Error Messages
public class LocalizedFilterEngine : IFilterEngine
{
private readonly IStringLocalizer _localizer;
public LocalizedFilterEngine(IStringLocalizer localizer)
{
_localizer = localizer;
}
// Implementation with localized error messages
}
📱 Platform Support
- .NET 7.0
- Not tested on lower and higher versions.
🤝 Contributing
We welcome contributions! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/mahmutcansaribal/FilterEngine.Core.git
cd FilterEngine.Core
dotnet restore
dotnet build
dotnet test
📄 License
This project is licensed under the MIT License
🆘 Support
- Issues: GitHub Issues
📈 Roadmap
v1.1.0 (Next Release)
- Async operations support
- Nested property filtering (e.g.,
Product.Category.Name)
🏆 Acknowledgments
- Built with ❤️ for the .NET community
- Inspired by OData and GraphQL filtering capabilities
- Thanks to all contributors and users
⭐ If this project helped you, please give it a star!
Made with ❤️ by Mahmut Can Saribal
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net7.0 is compatible. 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. |
-
net7.0
- Microsoft.EntityFrameworkCore (>= 7.0.20)
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 |
|---|