Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FlexQueryDemo/src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest'
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Works with Entity Framework Core, Dapper, and other FlexQuery.NET providers.
- **No OData Dependency** — Get powerful querying without OData's complexity and tight coupling
- **100% Server-Side** — All operations translate to SQL via expression trees — zero client evaluation
- **Security First** — Declare allowed/blocked fields per-endpoint with strict validation
- **Multi-Format** — Auto-detects DSL, JSON, JQL, and OData query syntax on the same endpoint
- **Multi-Format** — Auto-detects DSL, JSON, FQL, and OData query syntax on the same endpoint
- **Multiple Data Providers** — Works with EF Core, Dapper, or any IQueryable source

## Package Ecosystem
Expand All @@ -41,8 +41,9 @@ graph TD
Dapper --> Kendo
AspNet --> AgGrid
AspNet --> Kendo
Core --> Jql["FlexQuery.NET.Parsers.Jql"]
Core --> Fql["FlexQuery.NET.Parsers.Fql"]
Core --> OData["FlexQuery.NET.Parsers.MiniOData"]
AspNet --> OpenApi["FlexQuery.NET.OpenApi"]
```

| Package | Purpose |
Expand All @@ -54,14 +55,15 @@ graph TD
| [FlexQuery.NET.Diagnostics](https://www.nuget.org/packages/FlexQuery.NET.Diagnostics) | Execution diagnostics, timing, and observability |
| [FlexQuery.NET.Adapters.AgGrid](https://www.nuget.org/packages/FlexQuery.NET.Adapters.AgGrid) | AG Grid Server-Side Row Model (SSRM) request/response adapter |
| [FlexQuery.NET.Adapters.Kendo](https://www.nuget.org/packages/FlexQuery.NET.Adapters.Kendo) | Kendo UI DataSource request adapter |
| [FlexQuery.NET.Parsers.Jql](https://www.nuget.org/packages/FlexQuery.NET.Parsers.Jql) | JQL (Jira Query Language) syntax parser |
| [FlexQuery.NET.Parsers.Fql](https://www.nuget.org/packages/FlexQuery.NET.Parsers.Fql) | FQL (FlexQuery Language) syntax parser |
| [FlexQuery.NET.Parsers.MiniOData](https://www.nuget.org/packages/FlexQuery.NET.Parsers.MiniOData) | Lightweight OData-compatible syntax parser |
| [FlexQuery.NET.OpenApi](https://www.nuget.org/packages/FlexQuery.NET.OpenApi) | OpenAPI/Swagger documentation and examples for FlexQuery endpoints |

## Supported Query Formats

```http
GET /api/users?filter=age:gte:18&sort=name:asc&page=1&pageSize=20 # DSL (default)
GET /api/users?filter=Age >= 18 AND Status = 'Active' # JQL
GET /api/users?filter=Age >= 18 AND Status = 'Active' # FQL
GET /api/users?$filter=Age ge 18 and Status eq 'Active' # OData
```

Expand Down
57 changes: 40 additions & 17 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default defineConfig({
nav: [
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'Examples', link: '/examples/basic' },
{ text: 'Migration v3→v4', link: '/migration/v3-to-v4' },
{ text: 'Migration v2→v3', link: '/migration/v2-to-v3' },
{
text: 'Links',
Expand Down Expand Up @@ -77,14 +78,30 @@ export default defineConfig({
{ text: 'Query Formats', link: '/guide/query-formats' }
]
},
{
text: 'Comparisons',
items: [
{ text: 'vs GraphQL & OData', link: '/guide/comparison' },
{ text: 'vs .NET Libraries', link: '/guide/comparison-libraries' }
]
},
{
text: 'Architecture',
items: [
{ text: 'Provider Model', link: '/architecture/provider-model' },
{ text: 'Grouped Query Contract', link: '/architecture/grouped-query-contract' }
]
},
{
text: 'Execution & Security',
items: [
{ text: 'Execution Pipeline', link: '/guide/execution-pipeline' },
{ text: 'Validation', link: '/guide/validation' },
{ text: 'Security & Field Access', link: '/guide/security' },
{ text: 'Security Governance', link: '/guide/security-governance' },
{ text: 'Field Mapping', link: '/guide/field-mapping' }
{ text: 'Field Mapping', link: '/guide/field-mapping' },
{ text: 'Performance Tuning', link: '/guide/performance-tuning' },
{ text: 'Debugging', link: '/guide/debugging' }
]
},
{
Expand All @@ -104,22 +121,26 @@ export default defineConfig({
}
]
},
{
text: 'Parsers',
items: [
{ text: 'MiniOData Parser', link: '/parsers/miniodata' },
{ text: 'FQL Parser', link: '/parsers/fql' }
]
},
{
text: 'Adapters',
items: [
{ text: 'AG Grid Integration', link: '/adapters/ag-grid' },
{ text: 'MiniOData Parser', link: '/adapters/miniodata' },
{ text: 'Kendo UI Integration', link: '/adapters/kendo' },
{ text: 'Kendo UI Integration', link: '/adapters/kendo' }
]
},
{
text: 'Integration',
collapsed: false,
items: [
{ text: 'ASP.NET Core', link: '/guide/aspnet-integration' },
{ text: 'Swagger Integration', link: '/guide/swagger-integration' },
{ text: 'Performance Tuning', link: '/guide/performance-tuning' },
{ text: 'Debugging', link: '/guide/debugging' }
{ text: 'Swagger Integration', link: '/guide/swagger-integration' }
]
},
{
Expand All @@ -140,16 +161,14 @@ export default defineConfig({
]
},
{
text: 'Comparisons',
items: [
{ text: 'vs GraphQL & OData', link: '/guide/comparison' },
{ text: 'vs .NET Libraries', link: '/guide/dotnet-comparison' }
]
},
{
text: 'Architecture',
text: 'Examples',
items: [
{ text: 'Provider Model', link: '/guide/architecture/provider-model' }
{ text: 'Basic Examples', link: '/examples/basic' },
{ text: 'Advanced Examples', link: '/examples/advanced' },
{ text: 'Real-World Scenarios', link: '/examples/real-world' },
{ text: 'Format Examples', link: '/examples/formats' },
{ text: 'Filtering Examples', link: '/examples/filtering' },
{ text: 'Include Examples', link: '/examples/include' }
]
},
{
Expand All @@ -158,7 +177,8 @@ export default defineConfig({
{ text: 'Query Language', link: '/shared/query-language' },
{ text: 'Operators', link: '/shared/operators' },
{ text: 'Migration v1 → v2', link: '/migration' },
{ text: 'Migration v2 → v3', link: '/migration/v2-to-v3' }
{ text: 'Migration v2 → v3', link: '/migration/v2-to-v3' },
{ text: 'Migration v3 → v4', link: '/migration/v3-to-v4' }
]
}
],
Expand All @@ -169,7 +189,10 @@ export default defineConfig({
items: [
{ text: 'Basic Examples', link: '/examples/basic' },
{ text: 'Advanced Examples', link: '/examples/advanced' },
{ text: 'Real-World Scenarios', link: '/examples/real-world' }
{ text: 'Real-World Scenarios', link: '/examples/real-world' },
{ text: 'Format Examples', link: '/examples/formats' },
{ text: 'Filtering Examples', link: '/examples/filtering' },
{ text: 'Include Examples', link: '/examples/include' }
]
}
],
Expand Down
60 changes: 53 additions & 7 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,74 @@ import Layout from './Layout.vue'
import './custom.css'
import type { EnhanceAppContext } from 'vitepress'

function setPageMode(path: string): void {
const vpDoc = document.querySelector('.VPDoc')
if (!vpDoc) return

vpDoc.classList.remove('page-guide', 'page-performance', 'page-examples', 'page-comparison', 'page-reference', 'page-home')

if (path === '/' || path === '/index.html') {
vpDoc.classList.add('page-home')
return
}

if (path.startsWith('/guide/performance/')) {
vpDoc.classList.add('page-performance')
return
}

if (
path === '/guide/comparison' ||
path === '/guide/comparison-libraries' ||
path === '/comparison/graphql-odata' ||
path === '/guide/dotnet-comparison'
) {
vpDoc.classList.add('page-comparison')
return
}

if (path.startsWith('/examples/')) {
vpDoc.classList.add('page-examples')
return
}

if (path.startsWith('/guide/') || path.startsWith('/migration/')) {
vpDoc.classList.add('page-guide')
return
}

vpDoc.classList.add('page-reference')
}

export default {
...DefaultTheme,
// Layout,
enhanceApp({ router }: EnhanceAppContext) {
if (typeof window === 'undefined') return

router.onAfterRouteChanged = () => {
// Re-open the sidebar after navigation so it doesn't close on link click.
// VitePress uses a data attribute on <html> to track the sidebar open state.
const setPageModeWhenReady = (path: string) => {
const vpDoc = document.querySelector('.VPDoc')
if (vpDoc) {
setPageMode(path)
} else {
requestAnimationFrame(() => setPageModeWhenReady(path))
}
}

setPageModeWhenReady(decodeURIComponent(window.location.pathname))

router.onAfterRouteChanged = (to: string) => {
const el = document.querySelector('.VPSidebar') as HTMLElement | null
if (el) {
// On narrow viewports VitePress removes aria-expanded; we just ensure
// the sidebar keeps its visible class rather than being hidden.
el.style.display = ''
el.removeAttribute('inert')
}

// Keep the screen overlay clear — prevent the mask from re-appearing.
const mask = document.querySelector('.VPSidebarMask') as HTMLElement | null
if (mask) {
mask.style.display = 'none'
}

setPageModeWhenReady(to)
}
}
}
14 changes: 8 additions & 6 deletions docs/adapters/ag-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ AG Grid Enterprise (Browser)
ASP.NET Controller
AgGridQueryOptionsParser.Parse()
ToQueryOptions()
QueryOptions (canonical AST)
Expand All @@ -53,18 +53,21 @@ QueryResult<T>
## Basic Example

```csharp
using FlexQuery.NET.Adapters.AgGrid;
using FlexQuery.NET.Adapters.AgGrid.Models;

[HttpPost("grid-data")]
public async Task<IActionResult> GetGridData([FromBody] JsonElement agGridPayload)
{
var options = AgGridQueryOptionsParser.Parse(agGridPayload);
var options = agGridPayload.ToQueryOptions();

var result = await _context.Products.FlexQueryAsync<Product>(options, opts =>
var result = await _context.Products.FlexQueryAsync(options, opts =>
{
opts.AllowedFields = new HashSet<string> { "Id", "Name", "Price", "Category" };
opts.MaxPageSize = 200;
});

return Ok(result);
return Ok(options);
}
```

Expand Down Expand Up @@ -263,10 +266,9 @@ public class SalesController : ControllerBase
// 1. Parse the AG Grid payload
var options = request.ToQueryOptions();

// 2. Execute with Dapper
// 2. Execute with Dapper (dialect is auto-detected from the DbConnection)
var result = await ((DbConnection)_db).FlexQueryAsync<SalesRecord>(options, opts =>
{
opts.Dialect = new SqlServerDialect();
opts.AllowedFields = new HashSet<string>
{
"Id", "Product", "Category", "Region", "Amount", "SaleDate"
Expand Down
5 changes: 1 addition & 4 deletions docs/adapters/kendo.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ QueryResult<T>
[HttpPost("grid-data")]
public async Task<IActionResult> GetGridData([FromBody] JsonElement kendoPayload)
{
var options = KendoQueryOptionsParser.Parse(kendoPayload);
var options = kendoPayload.ToQueryOptions();

var result = await _context.Products.FlexQueryAsync<Product>(options, opts =>
{
Expand All @@ -76,9 +76,6 @@ Using extension methods:
```csharp
// From a KendoRequest object
var options = kendoRequest.ToQueryOptions();

// From a raw JSON string
var options = jsonString.FromKendoJson();
```

## Advanced Example: Full Kendo Payload
Expand Down
7 changes: 3 additions & 4 deletions docs/architecture/provider-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FlexQueryParameters (raw input from any source)
Parser Layer (shared)
├── JqlQueryParser
├── FQLQueryParser
├── JsonQueryParser
├── DslQueryParser
├── MiniODataQueryParser (optional)
Expand Down Expand Up @@ -43,7 +43,7 @@ FlexQueryParameters (raw input from any source)
│ ├── ApplySort() → OrderBy/ThenBy expressions
│ ├── ApplyPaging() → Skip/Take
│ ├── ApplySelect() → Dynamic projection
│ └── ApplyFilteredIncludes() → Include/ThenInclude
│ └── ApplyExpand() → Include/ThenInclude
│ │
│ ▼
│ EF Core → SQL (via database provider)
Expand Down Expand Up @@ -117,7 +117,7 @@ The validation pipeline checks:
| **Filtered includes** | ✅ Via `Include().Where()` | ✅ Via JOIN + WHERE |
| **Aggregation** | ✅ Via LINQ GroupBy | ✅ Via SQL GROUP BY |
| **SQL preview** | ✅ `ToSqlPreview()` | ✅ Access `SqlCommand.Sql` directly |
| **Dialect awareness** | ❌ Handled by EF provider | ✅ Explicit `ISqlDialect` |
| **Dialect awareness** | ❌ Handled by EF provider | ✅ Auto-detected from `DbConnection` |
| **Connection management** | ✅ Via DbContext | ❌ You manage `DbConnection` |
| **Migrations** | ✅ EF Migrations | ❌ Not applicable |

Expand Down Expand Up @@ -164,7 +164,6 @@ public async Task<IActionResult> GetProducts([FromQuery] FlexQueryParameters p)
public async Task<IActionResult> GetSalesReport([FromQuery] FlexQueryParameters p)
{
await using var conn = new SqlConnection(_connectionString);
await conn.OpenAsync();
return Ok(await conn.FlexQueryAsync<SalesRow>(p, ConfigureOptions));
}

Expand Down
8 changes: 4 additions & 4 deletions docs/comparison/graphql-odata.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ A quick side-by-side comparison. For the full analysis with complete request/res

## The Same Query

**Get active users named "alice", sorted by creation date, page 1:**
**Get active customers named "alice", sorted by creation date, page 1:**

### FlexQuery.NET
```
GET /api/users?filter=name:contains:alice,status:eq:active&sort=createdAt:desc&page=1&pageSize=10&select=id,name,email
GET /api/customers?filter=name:contains:alice,status:eq:active&sort=createdDate:desc&page=1&pageSize=10&select=id,name,email
```

### GraphQL
```graphql
POST /graphql
{ users(where: { and: [{ name: { contains: "alice" } }, { status: { eq: "active" } }] }, order: [{ createdAt: DESC }], skip: 0, take: 10) { totalCount items { id name email } } }
{ customers(where: { and: [{ name: { contains: "alice" } }, { status: { eq: "active" } }] }, order: [{ createdDate: DESC }], skip: 0, take: 10) { totalCount items { id name email } } }
```

### OData
```
GET /odata/Users?$filter=contains(Name,'alice') and Status eq 'active'&$orderby=CreatedAt desc&$top=10&$skip=0&$select=Id,Name,Email&$count=true
GET /odata/Customers?$filter=contains(Name,'alice') and Status eq 'active'&$orderby=CreatedDate desc&$top=10&$skip=0&$select=Id,Name,Email&$count=true
```

---
Expand Down
Loading
Loading