DynamicQuery Dynamic Query
DynamicQuery provides a set of lightweight, type-safe building blocks for generating LINQ filtering and sorting expressions at runtime based on DTO/query objects.
Namespace
Section titled “Namespace”using MiCake.Util.Query.Dynamic;Core Components
Section titled “Core Components”Filter - Single-Field Filter
Section titled “Filter - Single-Field Filter”var filter = Filter.Create( propertyName: "Name", values: new List<FilterValue> { FilterValue.Create("张三", ValueOperatorType.Contains) }, valuesJoinType: FilterJoinType.Or);Sort - Sorting
Section titled “Sort - Sorting”var sort = new Sort{ PropertyName = "CreateTime", Ascending = false // Descending};FilterValue - Filter Value
Section titled “FilterValue - Filter Value”var filterValue = FilterValue.Create( value: "test", operatorType: ValueOperatorType.Contains);Operator Types (ValueOperatorType)
Section titled “Operator Types (ValueOperatorType)”| Operator | Description | Example |
|---|---|---|
Equal |
Equal to | Name == "John" |
NotEqual |
Not equal to | Status != 0 |
Contains |
Contains | Name.Contains("phone") |
StartsWith |
Starts with | Name.StartsWith("Apple") |
EndsWith |
Ends with | Email.EndsWith("@example.com") |
GreaterThan |
Greater than | Price > 100 |
LessThan |
Less than | Stock < 10 |
GreaterThanOrEqual |
Greater than or equal to | Age >= 18 |
LessThanOrEqual |
Less than or equal to | Price <= 1000 |
Automatically Generating Filters with Attributes
Section titled “Automatically Generating Filters with Attributes”Defining a Query Object
Section titled “Defining a Query Object”public class ProductQueryDto : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.Contains)] public string? Name { get; set; }
[DynamicFilter(PropertyName = "Price", OperatorType = ValueOperatorType.GreaterThanOrEqual)] public decimal? MinPrice { get; set; }
[DynamicFilter(PropertyName = "Price", OperatorType = ValueOperatorType.LessThanOrEqual)] public decimal? MaxPrice { get; set; }
[DynamicFilter(OperatorType = ValueOperatorType.Equal)] public int? CategoryId { get; set; }}Generating Filter Conditions
Section titled “Generating Filter Conditions”var queryDto = new ProductQueryDto{ Name = "phone", MinPrice = 1000, MaxPrice = 5000, CategoryId = 5};
// Automatically generate filter conditionsFilterGroup filterGroup = queryDto.GenerateFilterGroup();
// The generated expression is equivalent to:// p => p.Name.Contains("phone")// && p.Price >= 1000// && p.Price <= 5000// && p.CategoryId == 5Applying to IQueryable
Section titled “Applying to IQueryable”var query = _dbContext.Products.AsQueryable();
// Apply dynamic filtersquery = query.ApplyFilters(filterGroup);
// Apply sortingquery = query.ApplySorting(new Sort{ PropertyName = "Price", Ascending = true});
// Execute the queryvar products = await query.ToListAsync();FilterGroup and CompositeFilterGroup
Section titled “FilterGroup and CompositeFilterGroup”FilterGroup - Filter Group
Section titled “FilterGroup - Filter Group”Combines multiple filter conditions:
var group = new FilterGroup{ Filters = new List<Filter> { Filter.Create("Name", new List<FilterValue> { FilterValue.Create("phone", ValueOperatorType.Contains) }), Filter.Create("Price", new List<FilterValue> { FilterValue.Create(1000m, ValueOperatorType.GreaterThanOrEqual) }) }, FiltersJoinType = FilterJoinType.And // AND join};
// Apply the filter groupvar query = _dbContext.Products.Filter(group);CompositeFilterGroup - Composite Filter Group
Section titled “CompositeFilterGroup - Composite Filter Group”Combines multiple FilterGroups:
var composite = new CompositeFilterGroup{ FilterGroups = new List<FilterGroup> { new FilterGroup { /* First group of conditions */ }, new FilterGroup { /* Second group of conditions */ } }, FilterGroupsJoinType = FilterJoinType.Or // OR join between groups};
// Apply the composite filtervar query = _dbContext.Products.Filter(composite);Nested Properties
Section titled “Nested Properties”Dot notation is supported for accessing nested properties:
public class OrderQueryDto : IDynamicQueryObj{ [DynamicFilter(PropertyName = "Customer.Name", OperatorType = ValueOperatorType.Contains)] public string? CustomerName { get; set; }
[DynamicFilter(PropertyName = "Address.City", OperatorType = ValueOperatorType.Equal)] public string? City { get; set; }}
// Generated expression:// o => o.Customer.Name.Contains("John") && o.Address.City == "Beijing"Class-Level Configuration
Section titled “Class-Level Configuration”Use [DynamicFilterJoin] to configure the class-level join type:
[DynamicFilterJoin(JoinType = FilterJoinType.And)] // Default is ANDpublic class ProductQuery : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.Contains)] public string? Name { get; set; }
[DynamicFilter(OperatorType = ValueOperatorType.Equal)] public int? CategoryId { get; set; }}
// Generates: Name.Contains("...") AND CategoryId == ...Usage Examples
Section titled “Usage Examples”Using in a Web API
Section titled “Using in a Web API”[ApiController][Route("api/[controller]")]public class ProductController : ControllerBase{ private readonly IRepository<Product, int> _repository;
[HttpGet] public async Task<List<Product>> Search([FromQuery] ProductQueryDto query) { var filterGroup = query.GenerateFilterGroup();
var products = await _repository.Query() .Filter(filterGroup) .Sort(new Sort { PropertyName = "CreateTime", Ascending = false }) .ToListAsync();
return products; }}Complex Queries
Section titled “Complex Queries”public class OrderQueryDto : IDynamicQueryObj{ // Fuzzy query on the order number [DynamicFilter(OperatorType = ValueOperatorType.Contains)] public string? OrderNumber { get; set; }
// Order status (multi-select) [DynamicFilter(OperatorType = ValueOperatorType.In)] public List<OrderStatus>? Statuses { get; set; }
// Amount range [DynamicFilter(PropertyName = "TotalAmount", OperatorType = ValueOperatorType.GreaterThanOrEqual)] public decimal? MinAmount { get; set; }
[DynamicFilter(PropertyName = "TotalAmount", OperatorType = ValueOperatorType.LessThanOrEqual)] public decimal? MaxAmount { get; set; }
// Date range [DynamicFilter(PropertyName = "CreateTime", OperatorType = ValueOperatorType.GreaterThanOrEqual)] public DateTime? StartDate { get; set; }
[DynamicFilter(PropertyName = "CreateTime", OperatorType = ValueOperatorType.LessThanOrEqual)] public DateTime? EndDate { get; set; }
// Customer name [DynamicFilter(PropertyName = "Customer.Name", OperatorType = ValueOperatorType.Contains)] public string? CustomerName { get; set; }}Dynamic Sorting
Section titled “Dynamic Sorting”[HttpGet]public async Task<List<Product>> GetProducts( [FromQuery] ProductQueryDto query, [FromQuery] string? sortBy = "CreateTime", [FromQuery] bool ascending = false){ var filterGroup = query.GenerateFilterGroup();
var products = await _repository.Query() .Filter(filterGroup) .Sort(new Sort { PropertyName = sortBy, Ascending = ascending }) .ToListAsync();
return products;}Multi-Field Sorting
Section titled “Multi-Field Sorting”var sorts = new List<Sort>{ new Sort { PropertyName = "Priority", Ascending = false }, new Sort { PropertyName = "CreateTime", Ascending = false }};
var query = _repository.Query() .Filter(filterGroup) .Sort(sorts);FilterExtensions Extension Methods
Section titled “FilterExtensions Extension Methods”| Method | Description |
|---|---|
Filter(Filter) |
Applies a single filter |
Filter(IEnumerable<Filter>) |
Applies multiple filters |
Filter(FilterGroup) |
Applies a filter group |
Filter(CompositeFilterGroup) |
Applies a composite filter group |
GetFilterExpression<T>() |
Gets the filter expression |
SortingExtensions Extension Methods
Section titled “SortingExtensions Extension Methods”| Method | Description |
|---|---|
Sort(Sort) |
Applies a single sort |
Sort(IEnumerable<Sort>) |
Applies multiple sorts |
Best Practices
Section titled “Best Practices”1. Use IDynamicQueryObj
Section titled “1. Use IDynamicQueryObj”// ✅ Correct: implement the interface to use the extension methodspublic class ProductQuery : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.Contains)] public string? Name { get; set; }}
// ❌ Wrong: doesn't implement the interfacepublic class ProductQuery{ public string? Name { get; set; }}2. Null Values Are Skipped Automatically
Section titled “2. Null Values Are Skipped Automatically”var query = new ProductQuery{ Name = "phone", // generates a filter condition CategoryId = null, // skipped automatically MinPrice = 0 // ⚠️ Note: 0 is not skipped};
// Only the Name filter condition is generatedvar filterGroup = query.GenerateFilterGroup();3. Use the In Operator
Section titled “3. Use the In Operator”public class OrderQuery : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.In)] public List<OrderStatus>? Statuses { get; set; }}
// Usagevar query = new OrderQuery{ Statuses = new List<OrderStatus> { OrderStatus.Pending, OrderStatus.Processing }};
// Generates: o => new[] { 0, 1 }.Contains(o.Status)4. Handle Nullable Types
Section titled “4. Handle Nullable Types”// ✅ Correct: use nullable typespublic class ProductQuery : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.Equal)] public int? CategoryId { get; set; } // Nullable}
// ❌ Wrong: non-nullable types have default value issuespublic class ProductQuery : IDynamicQueryObj{ [DynamicFilter(OperatorType = ValueOperatorType.Equal)] public int CategoryId { get; set; } // The default value 0 generates a filter condition}5. Combine Filter and Sort
Section titled “5. Combine Filter and Sort”// ✅ Correct: filter first, then sortvar result = await _repository.Query() .Filter(filterGroup) .Sort(sort) .Skip(skip) .Take(pageSize) .ToListAsync();
// ❌ Not recommended: sort first, then filtervar result = await _repository.Query() .Sort(sort) .Filter(filterGroup) // may affect the sorting result .ToListAsync();Implementation Details and Notes
Section titled “Implementation Details and Notes”- Null value skipping: null values are skipped automatically, no filter condition is generated
- Type conversion: uses
TypeDescriptor.GetConverterorSystem.Convert.ChangeType - Conversion failure: throws an
InvalidOperationException - In operator: automatically converted to a strongly-typed List
- Property access restriction: only public get properties are allowed
- Thread safety: generated expressions are thread-safe
Debugging Suggestions
Section titled “Debugging Suggestions”View the Generated Expression
Section titled “View the Generated Expression”var filterGroup = query.GenerateFilterGroup();var expression = _repository.Query().GetFilterExpression();
// Print the expressionConsole.WriteLine(expression?.ToString());// Output: p => (p.Name.Contains("phone") AndAlso (p.Price >= 1000))Test Boundary Values
Section titled “Test Boundary Values”// Test an empty stringvar query1 = new ProductQuery { Name = "" }; // will be skipped
// Test the value 0var query2 = new ProductQuery { MinPrice = 0 }; // will not be skipped
// Test an empty collectionvar query3 = new OrderQuery { Statuses = new List<OrderStatus>() }; // will be skippedImportant Notes
Section titled “Important Notes”- Null value handling: null, empty strings, and empty collections are skipped automatically
- Type safety: compile-time type checking avoids runtime errors
- EF Core compatibility: the generated expressions can be translated to SQL by EF Core
- Nested properties: dot notation is supported for accessing nested properties
- Thread safety: can be used safely in multi-threaded environments
