- Completed (Implementation Phase A fully executed)
- Current release phase: post-V1 Final / Evolution
- Execution window: June 2026 (Completed)
Define a safe and high-performance evolution path for TFluentQuery join capabilities, preserving ecosystem consistency and Dext principles:
- High performance
- Minimal/zero allocations in hot paths
- Backward compatibility
- Predictable behavior across ORM, specs, SQL generation, docs, and examples
This spec is intentionally separated from Issue #117 delivery.
Issue #117 remains the immediate focus for V1 RC1 (examples + docs).
Current join surface mixes two distinct execution models:
- SQL Join via
Join(table, alias, joinType, condition)(provider-translated SQL) - Generic Join via
Join<TInner, TKey, TResult>(in-memory correlation)
Issue #117 highlighted discoverability and usage confusion between these two models.
The following are not to be shipped in RC1:
- Large API redesign of
TFluentQuery - Breaking changes in existing Join signatures
- SQL parser expansion beyond simple, deterministic cases
- New behavior that risks ORM performance regressions before V1 Final
- Make SQL Join and in-memory Join behavior explicit in API and docs.
- Reduce ambiguity in naming and overload intent.
- Keep existing APIs source-compatible.
- Add explicit aliases (example:
JoinInMemory) while preserving legacyJoin<TInner,...>. - Add XML docs warning for in-memory materialization behavior.
Current SQL join shape:
.Join('order_items', 'oi', 'products.id = oi.product_id', jtInner)Proposed fluent/type-safe shape:
var p := Prototype.Entity<TProduct>;
var oi := Prototype.Entity<TOrderItem>; // default alias auto-generated
// 1. Explicit condition Join
Context.Entities<TProduct>
.AsNoTracking
.JoinInner<TOrderItem>(p.Id = oi.ProductId)
.Where(TProductType.Price > 10)
.ToList;
// 2. Implicit relationship-based Join (auto-resolves ON condition from metadata)
Context.Entities<TProduct>
.JoinInner<TOrderItem> // Automatically uses mapping: Product.Id = OrderItem.ProductId
.ToList;
// 3. Cross Join (no condition)
Context.Entities<TProduct>
.JoinCross<TOrderItem>
.ToList;Optional alias override:
Context.Entities<TProduct>
.JoinInner<TOrderItem>('oi', p.Id = oi.ProductId);- Prefer expressive DSL over multi-string/multi-parameter signatures.
- Keep join condition strongly typed through expression tree.
- Eliminate fragile string parsing from preferred path.
- Preserve low-level API for compatibility/interoperability.
- Automatically resolve ON conditions using Dext relationship metadata mapping (ForeignKey/Navigation attributes).
// Automatic ON condition resolution via metadata
function JoinInner<TInner: class>: TFluentQuery<T>; overload;
function JoinLeft<TInner: class>: TFluentQuery<T>; overload;
function JoinRight<TInner: class>: TFluentQuery<T>; overload;
function JoinFull<TInner: class>: TFluentQuery<T>; overload;
// Explicit ON conditions
function JoinInner<TInner: class>(const AOn: IExpression): TFluentQuery<T>; overload;
function JoinLeft<TInner: class>(const AOn: IExpression): TFluentQuery<T>; overload;
function JoinRight<TInner: class>(const AOn: IExpression): TFluentQuery<T>; overload;
function JoinFull<TInner: class>(const AOn: IExpression): TFluentQuery<T>; overload;
// Alias overrides + Automatic ON resolution
function JoinInner<TInner: class>(const AAlias: string): TFluentQuery<T>; overload;
function JoinLeft<TInner: class>(const AAlias: string): TFluentQuery<T>; overload;
function JoinRight<TInner: class>(const AAlias: string): TFluentQuery<T>; overload;
function JoinFull<TInner: class>(const AAlias: string): TFluentQuery<T>; overload;
// Alias overrides + Explicit ON conditions
function JoinInner<TInner: class>(const AAlias: string; const AOn: IExpression): TFluentQuery<T>; overload;
function JoinLeft<TInner: class>(const AAlias: string; const AOn: IExpression): TFluentQuery<T>; overload;
function JoinRight<TInner: class>(const AAlias: string; const AOn: IExpression): TFluentQuery<T>; overload;
function JoinFull<TInner: class>(const AAlias: string; const AOn: IExpression): TFluentQuery<T>; overload;
// Cross Joins (No ON condition)
function JoinCross<TInner: class>: TFluentQuery<T>; overload;
function JoinCross<TInner: class>(const AAlias: string): TFluentQuery<T>; overload;- Default alias must be deterministic and collision-safe.
- Suggested default strategy:
- start from type/table short name (
OrderItem->oi) - if collision, suffix increment (
oi2,oi3)
- start from type/table short name (
- Explicit alias always wins.
- Alias allocation must be cached in query build context to avoid repeated work.
- Keep ergonomic overload for simple
"left = right"scenarios. - Enforce strict and predictable parsing rules.
- Formalize grammar support for v1 helper:
- exactly one equality predicate
- no boolean chaining
- no function parsing
- Clear exception messages for invalid formats.
- Document that complex ON expressions must use
IExpression.
- Keep
Join(table, alias, condition, type)andJoin(..., condition: string, ...)as legacy/low-level path. - Mark strongly-typed
JoinInner/Left/Rightas preferred path in docs/examples. - Deprecation policy (non-breaking):
- Post-Final + 1 cycle: mark legacy path as "advanced/interop"
- Post-Final + 2+ cycles: evaluate soft deprecation warnings (no hard removal)
- Improve practical usability for joined result shapes without compromising performance.
- Evaluate typed projection helpers for SQL joins.
- Avoid runtime reflection-heavy projection in hot paths.
Evaluate additive projection patterns that keep SQL-side execution:
.SelectJoin<TResult>(...)Constraints:
- no per-row reflection in materialization loop
- generated projector cached by query signature/type tuple
- Improve explainability/debugging of query translation.
- Evaluate
ToQueryString()-style API for generated SQL + params. - Query tagging (
TagWith) for observability/tracing.
- Include resolved join metadata in debug trace:
- join type
- table and alias
- ON expression SQL
- Optional strict mode for ambiguity detection before execution when possible.
Any post-Final change must be validated across:
Sources/Data/Dext.Entity.Query.pasSources/Core/Dext.Specifications.*- SQL generator (
Dext.Specifications.SQL.Generator) - DbContext/DbSet query execution pipeline
- Examples (
Orm.EntityDemo, others) - Dext Book EN/PT-BR
- Unit/integration/performance test suites
- Prototype system (
Dext.Specifications.Typesand related builders) - Expression translators that consume prototype-origin expressions
- Documentation examples currently using raw
Join('table',...)
- No breaking signature removals in first post-Final iteration.
- Legacy behavior preserved unless clearly versioned/deprecated.
- New APIs must be additive and documented.
All proposed changes must satisfy:
- No extra allocations per-row in materialization paths.
- No reflection churn in tight loops (cache or pre-bind where possible).
- SQL Join pipeline overhead must remain effectively constant-time per query build step.
- In-memory Join path must document and preserve current complexity expectations.
- Join query build overhead (before/after)
- SQL generation overhead for joined specs
- In-memory Join throughput for medium/large sets
- Allocation snapshots (baseline vs proposed)
Use existing benchmark infra from S18.
- No transient string allocations in join condition hot path when using typed DSL.
- Alias resolution should allocate once per query build at most.
- Expression normalization for ON clause must use cached translators.
- SQL qualification logic for joined base columns must remain O(n columns) with no hidden quadratic behavior.
-
API confusion risk: same name, different execution model
Mitigation: explicit aliases + docs + examples -
Regression risk in SQL translation
Mitigation: golden tests for generated SQL and parameterization -
Performance risk from convenience APIs
Mitigation: micro-benchmarks + allocation guards -
Ecosystem drift risk (docs/examples/code mismatch)
Mitigation: synchronized update checklist in PR template
- Phase A (Safe Additive)
- Documentation and naming clarity
- Alias APIs (if approved)
- Non-breaking helper APIs
- Add typed
JoinInner/Left/Rightoverload set (with and without explicit alias) - Keep legacy join API fully functional
- Phase B (Diagnostics)
- Query explain/inspect API
- Optional tagging support
- Join-specific debug payloads (resolved aliases/ON SQL)
- Phase C (Ergonomic Projection)
- Typed SQL join projection patterns
- Performance validation and hardening
- Evaluate migration hints/tooling to promote typed DSL adoption
- Add typed join DSL as first-class documented approach.
- Keep legacy API intact and covered by tests.
- Update examples progressively:
- RC docs: show both, emphasize behavior differences
- Post-Final docs: put typed DSL first, legacy in advanced section
- Add analyzer-style docs checklist to PR templates:
- if new join example added, include SQL vs in-memory note
- include ambiguity-safe SQL assertions where relevant
- All Join modes are explicitly documented with execution model.
- No breaking changes to existing join consumers.
- Benchmarks show no unacceptable degradation (threshold from S18 policy).
- EN/PT-BR docs and examples remain synchronized.
- Unit + integration + example suite pass in CI.
While this spec defines the post-Final roadmap, the current active priority in RC1 is:
- Deliver Issue #117 with focused examples and documentation.