Skip to content
Closed
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
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,66 @@

All notable changes to this project will be documented in this file.

---
## [3.0.0] - 2026-05-15

### Added
- **.NET 10.0 Support**: Added support for `.NET 10.0` (`net10.0`) across all packages (`FlexQuery.NET`, `FlexQuery.NET.EFCore`, `FlexQuery.NET.Dapper`, `FlexQuery.NET.AspNetCore`, `FlexQuery.NET.MiniOData`).
- **Internal Options Base Class**: Introduced `BaseQueryExecutionOptions` to clean up and consolidate core options and security configuration fields.
- **Automatic OData Parser Discovery**: Implemented automatic reflection-based detection and registration of `MiniODataParser` within `QueryOptionsParser` to enable zero-configuration OData query parsing.
- **FlexQuery.NET.Dapper Package**:
- Full support for Dapper as a high-performance query engine.
- Polymorphic `ISqlDialect` system supporting SQL Server, PostgreSQL, MySQL, MariaDB, SQLite, and Oracle.
- Automatic dialect resolution via `ISqlDialectResolver` based on `DbConnection` types.
- Secure SQL translation engine with parameterization and identifier quoting.
- **Relationship Query Semantics for Dapper**:
- Implemented `any()`, `all()`, and `count()` semantics using efficient `EXISTS` and correlated subqueries.
- Support for `include` and Filtered Includes using `LEFT JOIN` syntax.
- Semantic parity with EF Core relationship queries.
- **Dapper AST & Translators**:
- Dedicated AST nodes for relationship queries, decoupled from core models.
- Specialized translators for Includes, Existence checks, and Counts.
- **Dapper Query Engine Stabilization**:
- Reimplemented `SqlCountTranslator` to use convention‑based `RelationshipMapping` via `IMappingRegistry`.
- Added missing `using FlexQuery.NET.Dapper.Mapping.Metadata` to `SqlExistsTranslator` and `SqlIncludeTranslator`.
- Fixed string interpolation issues and removed redundant `Metadata.` prefixes.
- Refactored Dapper mapping system to convention‑over‑configuration using `IMappingRegistry`.
- **Test Suite Integrity**:
- Updated `SqlTranslatorTests` to register `TestRole` (`ToTable("roles")`) and adjusted assertions for proper quoted SQL (`[roles].[UserId] = [users].[Id]`).
- Fixed navigation‑property test to expect table name `[TestEntities]`.
- Resolved CS1022 / CS1519 syntax errors in translation files.
- **FlexQuery.NET.MiniOData Package**:
- Lightweight OData-compatible query syntax adapter — completely optional, zero core dependencies.
- Parses `$filter`, `$orderby`, `$select`, `$top`, `$skip`, `$expand`, and `$count` into the unified FlexQuery AST.
- OData filter parser supporting binary comparisons (`eq`, `ne`, `gt`, `ge`, `lt`, `le`), function calls (`contains`, `startswith`, `endswith`), logical operators (`and`, `or`, `not`), grouping, null checks, `in` lists, and lambda navigation (`any`/`all`).
- Automatic OData path separator (`/`) to dot-notation conversion.
- `$` prefix stripping for seamless compatibility with both `$filter` and `filter` key formats.
- Lambda variable stripping for `any(o: o/status eq 'active')` expressions.
- DI registration via `services.AddFlexQueryMiniOData()`.
- Multi-targeting support for .NET 6, 8, and 10.
- **Mini OData ↔ Native DSL Equivalence**:
- 63 comprehensive tests verifying AST equivalence between Native DSL and Mini OData syntaxes.
- Proven semantic parity: both parsers produce identical `FilterGroup`, `SortNode`, and `QueryOptions` structures.
- Full solution test suite: 431 tests passing.

### Changed
- **JQL Status Promotion**: Removed obsolete/deprecation attributes from JQL parser components (`JqlParser`, `FlexQueryParameters.Query`, and `QuerySyntax.Jql`), reinstating JQL as a first-class supported query syntax option alongside DSL and OData.
- **Mini OData Cleanups**: Streamlined the dynamic registration calls and brought in namespaces natively in `ServiceCollectionExtensions`.
- **Dapper Parameter Binding**: Simplified and optimized clean parameter naming iteration in `FlexQueryDapperExtensions`.
- **Documentation Refactoring**: Reorganized the main `README.md` with new .NET 10.0 badges, dark-mode logo assets, and streamlined links.
- **Mapping Registry Evolution**: Updated `JoinInfo` to support `TargetType`, enabling deep property resolution for related entity filters in Dapper.
- **Dapper Multi-Targeting**: Updated support to target `.net6.0`, `.net8.0`, and `.net10.0` in the Dapper package (dropping EOL `.net7.0`).
- **Internal Reorganization**: Moved SQL translators to a dedicated `Translators` folder and namespace for better maintainability.

### Removed (Breaking Changes)
- **.NET 7.0 EOL Drop**: Dropped support for `.NET 7.0` (reached end-of-life on May 14, 2024) across all projects.
- **Obsolete APIs Cleanup**: Fully removed deprecated and legacy methods that were scheduled for removal:
- `ToQueryResultAsync` and `ToProjectedQueryResultAsync` from `QueryableEfCoreExtensions`.
- `ToQueryResult`, `ToProjectedQueryResult`, and `ApplyQueryOptions` from `QueryableExtensions`.
- `Validate` and `ApplyValidatedQueryOptions` overloads from `ValidationExtensions`.
- `QueryOptionsParser.Parse(QueryRequest)` helper from `QueryOptionsParser`.
- `SortOption` alias in favor of the unified `SortNode` class.

---
## [2.5.0] - 2026-05-10

Expand Down
2 changes: 2 additions & 0 deletions FlexQuery.NET.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<Project Path="src/FlexQuery.NET.EFCore/FlexQuery.NET.EFCore.csproj" />
<Project Path="src/FlexQuery.NET/FlexQuery.NET.csproj" />
<Project Path="src/FlexQuery.NET.AspNetCore/FlexQuery.NET.AspNetCore.csproj" />
<Project Path="src/FlexQuery.NET.Dapper/FlexQuery.NET.Dapper.csproj" />
<Project Path="src/FlexQuery.NET.MiniOData/FlexQuery.NET.MiniOData.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/FlexQuery.NET.Tests/FlexQuery.NET.Tests.csproj" />
Expand Down
108 changes: 45 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://raw.githubusercontent.com/peterjohncasasola/FlexQuery.NET/main/assets/logo.png" alt="FlexQuery.NET Logo" width="400">
<img src="https://raw.githubusercontent.com/peterjohncasasola/FlexQuery.NET/main/assets/logo-dark.png" alt="FlexQuery.NET Logo" width="400">
</p>

# FlexQuery.NET
Expand All @@ -8,7 +8,7 @@

[![NuGet Version](https://img.shields.io/nuget/v/FlexQuery.NET.svg)](https://www.nuget.org/packages/FlexQuery.NET)
[![NuGet Downloads](https://img.shields.io/nuget/dt/FlexQuery.NET.svg)](https://www.nuget.org/packages/FlexQuery.NET)
[![Dotnet Support](https://img.shields.io/badge/.NET-6.0%20%7C%207.0%20%7C%208.0-blueviolet)](https://dotnet.microsoft.com/download)
[![Dotnet Support](https://img.shields.io/badge/.NET-6.0%20%7C%208.0%20%7C%2010.0-blueviolet)](https://dotnet.microsoft.com/download)
[![Documentation](https://img.shields.io/badge/docs-vercel-blue.svg)](https://flexquery.vercel.app)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Expand All @@ -23,7 +23,6 @@ FlexQuery.NET is a lightweight and powerful dynamic query engine for .NET. It al
- **Advanced Projection**: Automatic SQL `SELECT` optimization including nested includes.
- **Governance & Security**: Built-in field-level validation and operator restrictions.
- **High Performance**: Thread-safe expression caching for ultra-low latency.
- **Explicit Joins**: SQL-like join support with alias-aware field resolution.

---

Expand All @@ -34,6 +33,7 @@ FlexQuery.NET is a lightweight and powerful dynamic query engine for .NET. It al
```bash
dotnet add package FlexQuery.NET
dotnet add package FlexQuery.NET.EFCore
dotnet add package FlexQuery.NET.Dapper
dotnet add package FlexQuery.NET.AspNetCore
```

Expand Down Expand Up @@ -62,79 +62,62 @@ public async Task<IActionResult> GetUsers([FromQuery] FlexQueryParameters parame
GET /api/users?filter=age:gt:18&sort=createdAt:desc&page=1&pageSize=20&select=id,name,email
```

---

## 🏎️ Performance Benchmarks

FlexQuery.NET is engineered for performance with transparent, reproducible benchmarks. We measure parsing, expression generation, full end-to-end execution, and API-level latency against Gridify, Sieve, OData, and GraphQL.

> For the complete benchmark suite with methodology, fairness disclaimers, and full analysis, see **[docs/guide/performance/](docs/guide/performance/)**.

### End-to-End Execution (EF Core InMemory, 1,000 records)

*Scenario: Filter (2 conditions) + Sort + Paging (100 items) on 1,000 Users with related Orders and OrderItems.*

| Library | Mean | Relative | Allocated |
|:---------|-----:|---------:|----------:|
| **FlexQuery.NET** | **17.67 ms** | **0.44×** | 21.42 KB |
| **Handwritten LINQ** | 40.21 ms | 1.00× | 97.11 KB |
| Gridify | 40.33 ms | 1.00× | 107.76 KB |
| System.Linq.Dynamic.Core | 40.95 ms | 1.02× | 110.79 KB |
| Sieve | 41.37 ms | 1.03× | 117.67 KB |
### 4. Dapper Integration & Database Dialects

FlexQuery.NET is **2.25× faster than handwritten LINQ** in this InMemory scenario, likely due to expression tree optimization and reduced closure allocation. Full analysis: [Execution Benchmarks](docs/guide/performance/execution.md).
FlexQuery.NET provides a robust Dapper extension (`FlexQuery.NET.Dapper`) that compiles queries into secure, parameterized, and database-specific SQL.

---

### Database Execution (SQL Server LocalDB, 100,000 records)

*Scenario: Simple filter (`status:eq:active`) + page (100 items) against SQL Server with no index.*

⚠️ **Important:** FlexQuery.NET's default configuration (`IncludeCount=true`) executes an additional COUNT query to return total record count, while the handwritten baseline retrieves data only. This benchmark therefore measures **two roundtrips vs one**. When configured fairly, FlexQuery.NET's filtering overhead is ~3–10% (see details).
#### Automatic Dialect Resolution

| Library | Mean | Relative | Allocated | Queries |
|:---------|-----:|---------:|----------:|---------|
| **Handwritten LINQ** (data only) | 336 µs | 1.00× | 111 KB | 1 SELECT |
| **FlexQuery.NET (with count)** | 20,798 µs | 61.8× | 129 KB | SELECT + COUNT |
By default, the SQL dialect is automatically resolved from your database connection (e.g., `SqlConnection` -> `SqlServerDialect`, `NpgsqlConnection` -> `PostgreSqlDialect`).

The apparent 62× overhead is the cost of the extra COUNT query. Full analysis, fair comparison methodology, and configuration options: [Database Execution](docs/guide/performance/database-execution.md).

---
```csharp
[HttpGet]
public async Task<IActionResult> GetUsersDapper([FromQuery] FlexQueryParameters parameters)
{
// The dialect is automatically resolved based on the provided NpgsqlConnection
using var connection = new NpgsqlConnection("Host=localhost;Database=mydb;");

var result = await connection.FlexQueryAsync<UserDto>(parameters, options =>
{
options.AllowedFields = ["Id", "Name", "Email"];
// Dapper specific options
options.CommandTimeoutSeconds = 60;
});

### API End-to-End (Full ASP.NET Core Pipeline, 100,000 records)
return Ok(result);
}
```

*Scenario: HTTP request with filter + sort + paging + projection, including JSON serialization.*
#### Explicit Dialect Configuration

| Library | PageSize=20 | PageSize=100 | PageSize=100K |
|:---------|------------:|-------------:|--------------:|
| **FlexQuery.NET** | 1.49 ms | 1.64 ms | 2.26 ms |
| GraphQL | 0.90 ms | 0.90 ms | FAILED |
| OData | 1.64 ms | 1.72 ms | 2.24 ms |
| Gridify | 1.56 ms | 1.90 ms | 1.90 ms |
| Sieve | 1.59 ms | 1.97 ms | 1.86 ms |
| Manual LINQ | 1.63 ms | 1.97 ms | 1.89 ms |
If you need to force a specific SQL dialect for a single query, you can configure it directly:

Full results with fairness notes: [API Benchmarks](docs/guide/performance/api-benchmarks.md).
```csharp
using FlexQuery.NET.Dapper.Dialects;

---
var result = await connection.FlexQueryAsync<UserDto>(parameters, options =>
{
// Explicitly configure the dialect for this specific query
options.Dialect = new MySqlDialect();
// Supported dialects: SqlServerDialect, PostgreSqlDialect, MySqlDialect, MariaDbDialect, SqliteDialect, OracleDialect
});
```

## 📚 Full Documentation
#### Global Dialect Configuration (Optional)

For detailed methodology, dataset description, reproducibility instructions, and fairness disclaimers:
If your entire application uses a single database type and you want to bypass the automatic resolution entirely, you can configure a global default dialect once at startup:

👉 **[Performance Documentation Index](docs/guide/performance/)**
```csharp
// Program.cs or Startup.cs
using FlexQuery.NET.Dapper;
using FlexQuery.NET.Dapper.Dialects;

- [Methodology & Reproducibility](docs/guide/performance/methodology.md)
- [Parsing Benchmarks](docs/guide/performance/parsing.md)
- [Expression Generation](docs/guide/performance/expression-generation.md)
- [End-to-End Execution](docs/guide/performance/execution.md)
- [Database Execution (SQL Server)](docs/guide/performance/database-execution.md)
- [API Benchmarks (vs OData/GraphQL)](docs/guide/performance/api-benchmarks.md)
- [Scalability Analysis](docs/guide/performance/scalability.md)
- [Fairness Disclaimers](docs/guide/performance/fairness-disclaimers.md)
- [Interpretation Guide](docs/guide/performance/interpretation-guide.md)
// Set the global dialect once for the entire application
DapperQueryOptions.GlobalDefaultDialect = new PostgreSqlDialect();

---
// Or, provide your own custom resolver logic:
// DapperQueryOptions.GlobalDialectResolver = new MyCustomResolver();
```

## 📚 Documentation

Expand All @@ -145,7 +128,6 @@ For detailed guides, API references, and advanced scenarios, visit our documenta
### Quick Links
- [Getting Started](https://flexquery.vercel.app/guide/getting-started)
- [Query Composition](https://flexquery.vercel.app/guide/composition)
- [Explicit Joins](https://flexquery.vercel.app/guide/joins)
- [Governance & Security](https://flexquery.vercel.app/guide/security)
- [Performance Optimization](https://flexquery.vercel.app/guide/performance-tuning)
- [Migration Guide (v1 → v2)](https://flexquery.vercel.app/migration/v1-to-v2)
Expand Down
Binary file added assets/logo-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using FlexQuery.NET.Dapper.Sql;

namespace FlexQuery.NET.Dapper.Configuration;

public static class FlexQueryDapperExtensions
{
/// <summary>
/// Configures FlexQuery.NET Dapper globally.
/// </summary>
public static IServiceCollection AddFlexQueryDapper(this IServiceCollection services, Action<DapperQueryOptions> configure)
{
var options = new DapperQueryOptions();
configure(options);

// Optionally, register the options as a singleton or configured options
services.AddSingleton(options);

if (options.Dialect != null)
{
DapperQueryOptions.GlobalDefaultDialect = options.Dialect;
}

return services;
}

/// <summary>
/// Configures the SQL Server dialect.
/// </summary>
public static DapperQueryOptions UseSqlServer(this DapperQueryOptions options)
{
options.Dialect = new Dialects.SqlServerDialect();
return options;
}

/// <summary>
/// Configures the PostgreSQL dialect.
/// </summary>
public static DapperQueryOptions UsePostgreSql(this DapperQueryOptions options)
{
options.Dialect = new Dialects.PostgreSqlDialect();
return options;
}

/// <summary>
/// Configures the SQLite dialect.
/// </summary>
public static DapperQueryOptions UseSqlite(this DapperQueryOptions options)
{
options.Dialect = new Dialects.SqliteDialect();
return options;
}
}
79 changes: 79 additions & 0 deletions src/FlexQuery.NET.Dapper/Conventions/DefaultEntityConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using FlexQuery.NET.Dapper.Mapping.Metadata;

namespace FlexQuery.NET.Dapper.Conventions;

/// <summary>
/// Default entity convention. Infers table name, maps properties, detects primary key.
/// </summary>
public class DefaultEntityConvention : IEntityConvention
{
private readonly IPluralizer _pluralizer;

public DefaultEntityConvention(IPluralizer pluralizer)
{
_pluralizer = pluralizer;
}

public void Apply(EntityMapping mapping)
{
var type = mapping.Type;

// 1. Table Name Convention
var tableAttr = type.GetCustomAttribute<TableAttribute>();
if (tableAttr != null)
{
mapping.TableName = tableAttr.Name;
}
else if (string.IsNullOrEmpty(mapping.TableName) || mapping.TableName == type.Name)
{
mapping.TableName = _pluralizer.Pluralize(type.Name);
}

// 2. Property Conventions
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Skip unmapped / ignored properties (can add NotMappedAttribute support here)
if (property.GetCustomAttribute<NotMappedAttribute>() != null)
continue;

// Skip navigation properties (complex types and collections are handled by relationship convention)
if (IsNavigationProperty(property.PropertyType))
continue;

var propMapping = mapping.GetOrAddProperty(property);

// Column Name
var columnAttr = property.GetCustomAttribute<ColumnAttribute>();
if (columnAttr != null)
{
propMapping.ColumnName = columnAttr.Name ?? property.Name;
}

// Primary Key
if (property.GetCustomAttribute<KeyAttribute>() != null)
{
propMapping.IsPrimaryKey = true;
}
else if (string.Equals(property.Name, "Id", StringComparison.OrdinalIgnoreCase) ||
string.Equals(property.Name, type.Name + "Id", StringComparison.OrdinalIgnoreCase))
{
propMapping.IsPrimaryKey = true;
}
}
}

private bool IsNavigationProperty(Type type)
{
if (type == typeof(string) || type == typeof(byte[]) || type.IsValueType || type.IsPrimitive)
return false;

// Nullable<T> where T is a value type is not a navigation property
if (Nullable.GetUnderlyingType(type) != null)
return false;

return true;
}
}
Loading
Loading