Skip to content
bitzorcas
中EN

Reference

Standard List Query Kernel and QueryShape

A deep architectural dive into the BitzOrcas.Modern standard list query kernel, uncovering compile-time execution plan generation, multi-table alias decoupling, custom basePredicate injection, and dual-ORM projection pushdown.

Last updated

In enterprise SaaS systems (especially in high-stakes legal, financial, and compliance domains), over 80% of high-frequency requests consist of online list paging and multi-dimensional filtering. Traditional architectures frequently suffer from two severe anti-patterns:

  1. The “Wide-Row In-Memory Deserialization” Anti-Pattern: Query handlers inject repositories to materialize full aggregate roots (containing heavy text columns, rich text, and JSON audit snapshots) into memory before mapping to DTOs via AutoMapper, resulting in massive GC pressure and database network I/O waste.
  2. The “Hardcoded String Table Alias” Fragile Anti-Pattern: In multi-table join queries, upper-layer DTO annotations hardcode physical table aliases (such as t.CaseNo, t4.OppositeName). Whenever the underlying repository join order changes or a new join table is inserted, the entire query pipeline instantly crashes at runtime due to table alias misalignment.

BitzOrcas.Modern establishes a declarative query paradigm via ADR 0303 (Standard List Query Kernel and QueryShape): declarative metadata drives compile-time query plans, combining 100% type safety, AOT zero reflection, complete decoupling of physical table aliases, safe custom business condition extension, and optimal dual-ORM (SqlSugar / EF Core) projection pushdown.

1. Concept: What is QueryShape?

1.1 Why Forbid Injecting IRepository<T> in Query Handlers?

This legacy pattern suffers from three critical flaws:

  • Broken Projection Pushdown: Hand-written repository calls easily pull unprojected entities, loading wide columns (e.g., full contract content, commentary history) directly into memory.
  • Dual-ORM Behavioral Drift: Subtle differences in LINQ-to-SQL translation between SqlSugar and EF Core (such as null propagation or string methods) introduce runtime bugs and portability failures.
  • Leaked Write Semantics: IRepository.FindAsync is a write-side aggregate restoration entry point (carrying change tracking or concurrency tokens) and must never be used for stateless read queries.

1.2 QueryShape Execution Lifecycle

Roslyn IncrementalGeneratorSeamless basePredicateInjectionAutomatic Alias Mapping &Projection

HTTP Request
(Fields + Paging + Sort)

Application ListQuery
(Envelope Record)

ICaseMatterReadModelStore
(Narrow Read Port)

*ListInput : IDeclareQueryShape
(Declarative Metadata DTO)

QueryDescriptor
(Typed Lambda Accessors)

QueryShapeExpressionBuilder
(Typed AST Expression Tree)

IQueryShapeExecutor
(SqlSugar / EF Core Executor)

Database Engine (SQL Server / PostgreSQL)

PagedResult<TDto>
(Pure Scalar Row Payload)

QueryShape is a compile-time metadata contract declaring:

  • Source / Root / Joins: The entity types for the root aggregate and joined relation tables;
  • Target Row (ReadModel): The flat scalar projection model constructed directly in the database query;
  • QueryFields: Whitelisted queryable fields, supported comparison operators, and entity member paths;
  • Sorts: Public sortable columns and tie-breaker sorting rules.

2. Compile-Time Metadata Generation

Inside src/Framework/BitzOrcas.QueryShape.Generator, the Roslyn incremental source generator analyzes classes decorated with [ReadModelQuery]:

Declarative QueryShape Example
// 1. Declare root aggregate and projection row, locking unique QueryShape identifier
[ReadModelQuery<CaseMatter, CaseMatterListRow>("Case.MatterList")]
// 2. Declare multi-level deterministic sorting rules
[ReadModelSort(nameof(SubmittedOn), Descending = true, Priority = 0)]
[ReadModelSort(nameof(MatterId), Descending = true, Priority = 1)]
public sealed partial class CaseMatterListInput : IDeclareQueryShape
{
// 3. Declare fuzzy search field generating SQL Contains
[QueryField(SourcePath = nameof(CaseMatter.Title), DefaultOperator = FilterOperator.Contains)]
public string? SearchText { get; init; }
// 4. Declare exact match status filter
[QueryField(SourcePath = nameof(CaseMatter.Status), DefaultOperator = FilterOperator.Equal)]
public MatterStatus? Status { get; init; }
// 5. Standard pagination parameters
public int PageIndex { get; init; }
public int PageSize { get; init; }
}

2.1 Four Core Compile-Time Guarantees

  1. Automatic IDeclareQueryShape Implementation: Emits CaseMatterListInput.QueryShape.g.cs with static QueryDescriptor, translating property mappings into pure Lambda trees without runtime reflection.
  2. Single Parameter Normalization: When combining Where filters, the engine rewrites sub-expressions to share a single root ParameterExpression via ParameterReplacementVisitor, producing clean AndAlso / OrElse nodes and eliminating untranslatable Expression.Invoke nodes.
  3. Automated Tenancy and Soft Delete Pushdown: For entities implementing ITenantEntity, ORM global filters (SqlSugar AddTableFilter and EF Core HasQueryFilter) automatically inject tenancy and soft-delete filters. Developers must never manually append !item.IsDeleted or item.TenantId == tenantId.
  4. Compile-Time Security Boundary: Properties not explicitly decorated with [QueryField] are rejected from the whitelist, preventing malicious query injection.

3. Multi-Table Join & Complete Table Alias Decoupling

3.1 Why DTO Annotations Never Need Physical Table Prefixes

In legacy query frameworks relying on string condition splicing (such as SqlSugar’s string ConditionalModel), DTO properties had to hardcode physical table aliases:

[Anti-Pattern] Hardcoding Table Aliases (Forbidden)
// DANGEROUS: tightly couples to Join order and alias t4
[AdvancedWhere(FieldNames = new[] { "t4", nameof(CaseMatterRelatedParty.OppositeName) })]
public string? OppositeName { get; set; }

This pattern is fundamentally fragile: If requirements shift and developers insert another join (such as Office), the party table shifts from table 4 to table 5. If developers miss updating the t4 annotation, production queries break silently or return corrupted data.

3.2 The Decoupling Architecture: From Entity Paths to Adaptive AST

In BitzOrcas.Modern, DTO metadata references domain entities exclusively; prefixes like t., t1., or t4. are completely forbidden:

[Recommended] Pure Entity Member Paths
// Directly map to root aggregate or joined entity properties
[QueryField(SourcePath = nameof(CaseMatter.ClientName))]
public string? ClientName { get; init; }
[QueryField(SourcePath = nameof(CaseMatterRelatedParty.OppositeName))]
public string? OppositeName { get; init; }

Under the Hood Mechanism:

  1. Compile Time: The source generator emits typed single-source accessors bound to the entity type:
    // Bound solely to entity type, completely independent of query join order
    new QueryFieldAccessor("matter", "ClientName", (Expression<Func<CaseMatter, string>>)(m => m.ClientName))
  2. Execution Time (QueryShapeExpressionBuilder): When building a two-source query, the builder allocates parameter expressions root = Expression.Parameter(typeof(TRoot)) and join = Expression.Parameter(typeof(TJoin)), weaving them into a strongly-typed two-parameter Lambda: Expression<Func<TRoot, TJoin, bool>>.
  3. ORM Adaptive Resolution: In SqlSugar, SqlSugarJoinQueryShapeExecutor hands this Lambda directly to ISugarQueryable<TRoot, TJoin>.Where(...). SqlSugar’s expression parser maps tables according to parameter types and positional indices, automatically emitting the correct SQL aliases ([t], [t1]).
  4. Total Decoupling: Modifying the repository join order or adding join tables requires zero changes to DTOs, completely eliminating alias mismatch bugs.

4. Custom Conditions (basePredicate) and Out-of-Band Business Logic

While QueryShape automates 90% of standardized list queries, enterprise applications often encounter specialized boundaries:

  • User Data Isolation (e.g., scoping matters assigned to the authenticated lead attorney);
  • Adversary Conflict Checks (e.g., filtering out restricted counter-parties);
  • Dynamic State Machine Logic (e.g., omitting deleted drafts or archived records).

BitzOrcas.Modern natively supports injecting strongly-typed basePredicate expressions into the QueryShape pipeline.

4.1 Single-Source and Two-Source basePredicate Examples

Injecting Typed Base Predicates in Read Store
public async Task<Result<PagedResult<CaseMatterSummaryDto>>> SearchMattersAsync(
CaseMatterListFields fields,
PaginationParams paging,
string currentLawyerId,
bool includeRestrictedArchive,
CancellationToken cancellationToken = default)
{
var input = CaseMatterListInput.From(fields, paging);
// Single-source query: inject strongly-typed single-entity predicate
return await input.ExecutePageAsync(
executorFactory,
static row => new CaseMatterSummaryDto(row.MatterId, row.Title, row.ClientName, row.Status),
basePredicate: matter =>
// 1. User scope isolation
matter.LeadLawyerId == currentLawyerId
// 2. Dynamic state machine logic
&& (includeRestrictedArchive || matter.Status != MatterStatus.Archived),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
Two-Source Join Query with Custom Predicate
// Two-source join: leverage two-parameter typed Lambda with 100% type safety
return await joinInput.ExecutePageAsync<CaseMatter, CaseMatterRelatedParty, CaseMatterSummaryDto>(
joinExecutorFactory,
static (matter, party) => new CaseMatterSummaryDto(matter.Id, matter.Title, party.OppositeName),
// Custom cross-entity business conditions: zero string aliases needed
basePredicate: (matter, party) =>
matter.OfficeId == currentOfficeId
&& (party.IsActive || party.RiskLevel < RiskLevel.High),
cancellationToken: cancellationToken).ConfigureAwait(false);

4.2 Why Custom basePredicate Remains 100% Robust

  1. Refactoring Type Safety: Written as C# Lambda expressions, entity property renames or type changes are validated by the compiler at build time.
  2. Strict Dual-ORM Parity:
    • SqlSugar: Executed directly via ISugarQueryable<TRoot, TJoin>.Where(basePredicate).
    • EF Core: Rebound to row.Root.xxx and row.Joined.xxx via JoinRowParameterVisitor.
  3. Seamless Merge with DTO Filters: The executor merges DTO filters and basePredicate into a single, unified SQL WHERE clause using Expression.AndAlso.

Below is a complete, production-grade slice covering HTTP envelopes, narrow read store ports, QueryShape declarations, custom predicates, and scalar projections:

5.1 Application Layer: Query Envelope & Validation Rule

src/Modules/Case/Application/Queries/ListCaseMattersQuery.cs
using BitzOrcas.Application.Abstractions;
using BitzOrcas.Application.Authorization;
using BitzOrcas.Case.Contracts;
using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Domain.Results;
using BitzOrcas.Endpoint.Attributes;
using Mediator;
namespace BitzOrcas.Case.Application.Queries;
/// <summary>
/// Paginated case matter search query command
/// </summary>
[GenerateEndpoint(
HttpRoute.Query,
"/api/cases/matters",
Tag = "CaseMatter",
QueryShapeName = "Case.MatterList")]
public sealed record ListCaseMattersQuery(
CaseMatterListFields Fields,
PaginationParams Paging,
SortRequest? Sort = null) :
IQuery<Result<PagedResult<CaseMatterSummaryDto>>>,
IAuthorizedRequest,
IPaginatedRequest
{
// Authorization metadata binding
public ResourceDescriptor Resource { get; } = new(CasePermissions.Module, CasePermissions.MatterResource);
public AuthorizationAction Action { get; } = AuthorizationAction.Read;
}
/// <summary>
/// Query input validation rule (Tier 1 Base Invariant)
/// </summary>
public sealed class ListCaseMattersQueryRule : IRequestRule<ListCaseMattersQuery>
{
public ValueTask<Result> ValidateAsync(ListCaseMattersQuery request, CancellationToken cancellationToken)
{
// Prevent slow queries caused by excessively long search text
if (request.Fields.SearchText is { Length: > CaseMatterListFields.SearchTextMaxLength })
{
return ValueTask.FromResult(Result.Failure(CaseErrors.SearchTextTooLong));
}
// Validate page index
if (request.Paging.PageIndex < 1)
{
return ValueTask.FromResult(Result.Failure(CaseErrors.InvalidPageIndex));
}
return ValueTask.FromResult(Result.Success());
}
}
/// <summary>
/// Query handler delegating typed envelope to narrow read store
/// </summary>
public sealed class ListCaseMattersQueryHandler(ICaseMatterReadModelStore store)
: IQueryHandler<ListCaseMattersQuery, Result<PagedResult<CaseMatterSummaryDto>>>
{
public async ValueTask<Result<PagedResult<CaseMatterSummaryDto>>> Handle(
ListCaseMattersQuery request,
CancellationToken cancellationToken)
{
// Delegate to narrow read store without exposing ORM details
return await store.SearchAsync(
request.Fields,
request.Paging,
request.Sort,
cancellationToken).ConfigureAwait(false);
}
}

5.2 Contracts Layer: Narrow Read Port & Business DTOs

src/Modules/Case/Contracts/ICaseMatterReadModelStore.cs
using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Case.Contracts;
/// <summary>
/// Narrow read-only store port (forbids exposing broad repositories or IQueryable)
/// </summary>
public interface ICaseMatterReadModelStore
{
Task<Result<PagedResult<CaseMatterSummaryDto>>> SearchAsync(
CaseMatterListFields fields,
PaginationParams paging,
SortRequest? sort,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Business filtering fields package
/// </summary>
public sealed record CaseMatterListFields(
string? SearchText = null,
string? ClientName = null,
MatterStatus? Status = null,
DateRange? SubmittedRange = null)
{
public const int SearchTextMaxLength = 200;
}
/// <summary>
/// Projected read model DTO
/// </summary>
public sealed record CaseMatterSummaryDto(
string MatterId,
string SerialId,
string Title,
string ClientName,
MatterStatus Status,
DateTimeOffset SubmittedOn);

5.3 Infrastructure Layer: QueryShape & ReadModelStore Implementation

src/Modules/Case/Infrastructure/CaseMatterReadModelStore.cs
using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.Case.Contracts;
using BitzOrcas.Case.Domain;
using BitzOrcas.DI.Attributes;
using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Domain.Results;
using BitzOrcas.Infrastructure.Queries;
namespace BitzOrcas.Case.Infrastructure;
[RegisterPersistenceAdapter<ICaseMatterReadModelStore>]
public sealed class CaseMatterReadModelStore(
IQueryShapeExecutorFactory executorFactory,
ICurrentTenant currentTenant) : ICaseMatterReadModelStore
{
public async Task<Result<PagedResult<CaseMatterSummaryDto>>> SearchAsync(
CaseMatterListFields fields,
PaginationParams paging,
SortRequest? sort,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(fields);
ArgumentNullException.ThrowIfNull(paging);
// Verify ambient tenant context
if (!currentTenant.Tenant.IsAvailable)
{
return Result.Failure<PagedResult<CaseMatterSummaryDto>>(CaseErrors.TenantRequired);
}
// 1. Perform single-point pagination normalization in From mapper
var input = CaseMatterListInput.From(fields, paging);
// 2. Execute QueryShape pushdown query with custom predicate
return await input.ExecutePageAsync(
executorFactory,
static row => new CaseMatterSummaryDto(
row.MatterId,
row.SerialId,
row.Title,
row.ClientName,
row.Status,
row.SubmittedOn),
basePredicate: matter => matter.Status != MatterStatus.DeletedDraft,
sorts: sort.WithTieBreaker(nameof(CaseMatterListInput.MatterId)),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Case matter QueryShape declaration
/// </summary>
[ReadModelQuery<CaseMatter, CaseMatterListRow>("Case.MatterList")]
[ReadModelSort(nameof(SubmittedOn), Descending = true, Priority = 0)]
[ReadModelSort(nameof(MatterId), Descending = true, Priority = 1)]
public sealed partial class CaseMatterListInput : IDeclareQueryShape
{
public static CaseMatterListInput From(CaseMatterListFields fields, PaginationParams paging)
{
// Safe single-point normalization preventing HTTP 400 errors
var normalized = paging.Normalize();
return new CaseMatterListInput
{
SearchText = string.IsNullOrWhiteSpace(fields.SearchText) ? null : fields.SearchText.Trim(),
ClientName = string.IsNullOrWhiteSpace(fields.ClientName) ? null : fields.ClientName.Trim(),
Status = fields.Status,
SubmittedFrom = fields.SubmittedRange?.From,
SubmittedTo = fields.SubmittedRange?.To,
PageIndex = normalized.PageIndex,
PageSize = normalized.PageSize,
};
}
[QueryField(SourcePath = nameof(CaseMatter.Title), Default = true, DefaultOperator = FilterOperator.Contains)]
public string? SearchText { get; init; }
[QueryField(SourcePath = nameof(CaseMatter.ClientName), DefaultOperator = FilterOperator.Contains)]
public string? ClientName { get; init; }
[QueryField(SourcePath = nameof(CaseMatter.Status), DefaultOperator = FilterOperator.Equal)]
public MatterStatus? Status { get; init; }
[QueryField(SourcePath = nameof(CaseMatter.SubmittedOn), DefaultOperator = FilterOperator.GreaterThanOrEqual)]
public DateTimeOffset? SubmittedFrom { get; init; }
[QueryField(SourcePath = nameof(CaseMatter.SubmittedOn), DefaultOperator = FilterOperator.LessThanOrEqual)]
public DateTimeOffset? SubmittedTo { get; init; }
[QueryField(Filterable = false, Visible = false)]
public DateTimeOffset SubmittedOn { get; init; }
[QueryField(SourcePath = nameof(CaseMatter.Id), Filterable = false, Visible = false)]
public string? MatterId { get; init; }
public int PageIndex { get; init; }
public int PageSize { get; init; }
}
/// <summary>
/// Pure scalar projection row model
/// </summary>
internal sealed record CaseMatterListRow
{
[ReadModelProjection(nameof(CaseMatter.Id))]
public string MatterId { get; init; } = string.Empty;
public string SerialId { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty;
public string ClientName { get; init; } = string.Empty;
public MatterStatus Status { get; init; }
public DateTimeOffset SubmittedOn { get; init; }
}

6. Unit Testing & HTTP Payloads

6.1 Unit Test Golden Sample: Validating QueryShape and Custom Predicates

tests/BitzOrcas.Unit.Tests/Case/CaseMatterQueryShapeTests.cs
using System.Linq.Expressions;
using BitzOrcas.Case.Contracts;
using BitzOrcas.Case.Domain;
using BitzOrcas.Case.Infrastructure;
using BitzOrcas.Domain.Abstractions.Queries;
using BitzOrcas.Infrastructure.Queries;
using Shouldly;
using Xunit;
namespace BitzOrcas.Unit.Tests.Case;
public sealed class CaseMatterQueryShapeTests
{
[Fact]
public void CaseMatterListInput_Should_Declare_Accurate_QueryDescriptor_Without_Reflection()
{
// 1. Validate compile-time generated IDeclareQueryShape descriptor
var descriptor = ((IDeclareQueryShape)new CaseMatterListInput()).QueryDescriptor;
descriptor.ShouldNotBeNull();
descriptor.Shape.Name.ShouldBe("Case.MatterList");
// 2. Validate field whitelist and operator generation
var titleField = descriptor.Shape.Fields.FirstOrDefault(f => f.InputField == nameof(CaseMatterListInput.SearchText));
titleField.ShouldNotBeNull();
titleField.Allows(FilterOperator.Contains).ShouldBeTrue();
// 3. Validate typed accessor availability
var accessor = descriptor.ResolveAccessor("item", nameof(CaseMatter.Title));
accessor.IsSuccess.ShouldBeTrue();
}
[Fact]
public void BasePredicate_Should_Correctly_Filter_Deleted_Draft_Matters()
{
// Validate custom basePredicate expression logic
Expression<Func<CaseMatter, bool>> basePredicate = matter => matter.Status != MatterStatus.DeletedDraft;
var compiled = basePredicate.Compile();
var activeMatter = new CaseMatter { Status = MatterStatus.Active };
var draftMatter = new CaseMatter { Status = MatterStatus.DeletedDraft };
// Assert business filtering behavior
compiled(activeMatter).ShouldBeTrue();
compiled(draftMatter).ShouldBeFalse();
}
}

6.2 HTTP Request and Response JSON Payloads

HTTP POST /api/cases/matters (or GET with query package)
{
"fields": {
"searchText": "Intellectual Property",
"status": "Active",
"submittedRange": {
"from": "2026-01-01T00:00:00Z",
"to": "2026-12-31T23:59:59Z"
}
},
"paging": {
"pageIndex": 1,
"pageSize": 20
},
"sort": {
"field": "submittedOn",
"descending": true
}
}
HTTP 200 OK Response Payload
{
"isSuccess": true,
"data": {
"items": [
{
"matterId": "matter-01912a3b-4c5d-7e8f-9a0b-1c2d3e4f5a6b",
"serialId": "CASE-2026-0889",
"title": "Cross-Border Patent Infringement First Instance",
"clientName": "Apex Tech Solutions Inc.",
"status": "Active",
"submittedOn": "2026-08-20T14:30:00Z"
}
],
"totalCount": 1,
"pageIndex": 1,
"pageSize": 20,
"totalPages": 1,
"hasPreviousPage": false,
"hasNextPage": false
}
}

7. Pagination Normalization and Limits (Defense in Depth)

In production environments, frontends may occasionally send invalid parameters (e.g., pageIndex = 0 or pageSize = 5000). BitzOrcas.Modern enforces a Defense in Depth pagination normalization contract:

PaginationParams Normalization Contract
public sealed record PaginationParams(
int PageIndex = 1,
int PageSize = PagingLimits.DefaultPageSize)
{
// Execute safe boundary normalization
public PaginationParams Normalize()
=> new(
PagingLimits.NormalizePageIndex(PageIndex),
PagingLimits.NormalizePageSize(PageSize));
}

7.1 PagingLimits Core Rules

ScenarioInputNormalized OutputDesign Rationale
Non-positive page indexpageIndex <= 01Paging starts at 1, preventing negative SQL offset
Non-positive page sizepageSize <= 020 (DefaultPageSize)Fall back to platform default
Valid page size1 <= pageSize <= 1000UnchangedNormal operational range
Exceeding page sizepageSize > 10001000 (MaxPageSize)Safely truncate to 1000 without returning 400 error!

8. Summary & Best Practices Checklist

  1. Never hardcode physical table prefixes in DTOs: Avoid t. or t1. in [QueryField]; declare pure aggregate root entity member paths.
  2. Enforce Read/Write Segregation: Inject I*ReadModelStore in query handlers; never expose broad repositories or IQueryable.
  3. Accurate Scalar Projections: Use ReadModelProjection row records to select exact columns, eliminating wide-row materialization.
  4. Typed Custom Conditions: Inject out-of-band and scope logic through basePredicate Lambdas to maintain 100% compile-time type safety across both SqlSugar and EF Core.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%