Using FlexQuery with Existing LINQ Queries
FlexQuery.NET is built on top of standard IQueryable. Because it uses standard expression trees, it doesn't need to be the "owner" of the query. You can chain FlexQuery with your existing LINQ logic seamlessly.
Chaining with Base Queries
In many production scenarios, you don't want to expose your entire database to the client. You might have global filters for soft deletes, multi-tenancy, or permissions.
You can apply these filters using standard LINQ .Where() calls before passing the query to FlexQuery.
Basic Example (The Secure Way)
Using ApplyValidatedQueryOptions is the recommended way to apply dynamic logic. It automatically runs the validation pipeline and throws a QueryValidationException if the client input is malicious or malformed.
[HttpGet]
public async Task<IActionResult> Get([FromQuery] QueryRequest request)
{
var options = QueryOptionsParser.Parse(request);
// 1. Define your base query with fixed logic
var baseQuery = _context.Products
.Where(p => !p.IsDeleted)
.Where(p => p.TenantId == _currentUser.TenantId);
// 2. Layer FlexQuery on top securely
// This will throw if 'options' contains forbidden fields or invalid operators
var results = await baseQuery
.ApplyValidatedQueryOptions(options)
.ToListAsync();
return Ok(results);
}Granular Pipeline Control
If you need more control over when specific parts of the query options are applied, FlexQuery provides granular extension methods.
Applying Filters & Sorting Separately
You can use ApplyFilter and ApplySort to control exactly where in your pipeline the dynamic logic is injected.
var query = _context.Users
.ApplyFilter(options) // Only apply dynamic WHERE
.Where(u => u.Email.EndsWith("@company.com")) // Hard-coded filter after
.ApplySort(options); // Only apply dynamic ORDER BYSample Generated SQL:
SELECT * FROM Users
WHERE (Age > 25) -- Dynamic filter from ApplyFilter
AND (Email LIKE '%@company.com') -- Hard-coded filter from .Where()
ORDER BY CreatedDate DESC -- Dynamic sort from ApplySortManual Paging & Projection
If you need to perform actions between paging and projection (like custom logging or additional counting), you can split them up.
var query = _context.Orders
.ApplyFilter(options)
.ApplySort(options);
var total = await query.CountAsync(); // Get total count before paging
var results = await query
.ApplyPaging(options) // Apply dynamic SKIP/TAKE
.ApplySelect(options) // Apply dynamic SELECT (returns IQueryable<object>)
.ToListAsync();Sample Generated SQL:
SELECT Id, OrderNumber, TotalAmount -- From dynamic Select
FROM Orders
WHERE ...
ORDER BY ...
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY -- From dynamic PagingEF Core: Filtered Includes
If you are using Entity Framework Core, you can use ApplyFilteredIncludes to apply dynamic filters to related collections within an Include statement. This is useful for "Search inside Include" scenarios.
// Example Request: ?include=Orders(status = "Shipped") or ?include=Orders(status:eq:"Shipped")
var options = QueryOptionsParser.Parse(Request.Query);
var customers = await _context.Customers
.ApplyFilteredIncludes(options) // Applies the filtered navigation
.ApplyValidatedQueryOptions(options) // Applies root filters/paging
.ToListAsync();Sample Generated SQL:
SELECT c.Id, c.Name, o.Id, o.Status
FROM Customers c
LEFT JOIN (
SELECT * FROM Orders WHERE Status = 'Shipped' -- Filtered Include logic
) o ON c.Id = o.CustomerId
WHERE ...Combining Dynamic Inputs
When you use a query string like ?filter=age:gt:25&sort=createdDate:desc, FlexQuery translates these into additional LINQ expressions.
Example Request
GET /api/users?filter=age:gt:25&sort=createdDate:desc
If your code looks like this:
var query = _context.Users
.Where(u => u.IsActive); // Base condition
var results = await query
.ApplyValidatedQueryOptions(options)
.ToListAsync();The final SQL generated by EF Core will look something like:
SELECT * FROM Users
WHERE IsActive = 1 -- From your .Where()
AND Age > 25 -- From FlexQuery filter
ORDER BY CreatedDate DESC -- From FlexQuery sortFlexQuery respects the IQueryable pipeline, ensuring that all conditions are combined into a single, efficient database query.
Why This Matters
- Reusable Logic: Define common filters (like
IsDeleted) once in a base repository or service and let FlexQuery handle the dynamic parts. - Less Boilerplate: You don't need to write manual
ifstatements to check if a user provided a filter; FlexQuery handles the optionality while you maintain control over the mandatory logic. - Clean Separation: Keep your business rules (who can see what) in your code and your UI flexibility (how they see it) in the query string.
Notes & Limitations
- No Overrides: FlexQuery cannot "undo" a filter you've already applied to the
IQueryable. If you filter forStatus == "Active", a client filter forStatus == "Inactive"will simply return zero results. - Provider Dependencies: While FlexQuery generates standard expressions, the final translation depends on your LINQ provider (EF Core, CosmosDB, etc.). Always verify that your base query and dynamic query together result in translatable SQL.
Best Practices
- Soft Deletes: Always apply
.Where(x => !x.IsDeleted)as a base query to prevent dynamic filters from accidentally exposing deleted records. - Multi-Tenancy: Hard-code your
TenantIdfilter before calling FlexQuery. This is a critical security boundary that should never be handled dynamically by client input. - Role-Based Access: If certain users should only see specific categories of items, apply those category filters in your C# logic first.
