Skip to content

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.

csharp
[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.

csharp
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 BY

Sample Generated SQL:

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 ApplySort

Manual Paging & Projection

If you need to perform actions between paging and projection (like custom logging or additional counting), you can split them up.

csharp
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:

sql
SELECT Id, OrderNumber, TotalAmount -- From dynamic Select
FROM Orders
WHERE ...
ORDER BY ...
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY -- From dynamic Paging

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

csharp
// 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:

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:

csharp
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:

sql
SELECT * FROM Users 
WHERE IsActive = 1      -- From your .Where()
  AND Age > 25          -- From FlexQuery filter
ORDER BY CreatedDate DESC -- From FlexQuery sort

FlexQuery 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 if statements 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 for Status == "Active", a client filter for Status == "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 TenantId filter 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.

Released under the MIT License.