Table of Contents

Choosing a select declaration

1.0 decision

Retain the existing select attributes and named partial methods. Do not introduce an InquirySelect rename for 1.0. Prefer explicit nameof fields over deriving query meaning from a method name.

Use InquirySelectAllByPredicate plus InquiryWhere as the canonical general filtering declaration. Use InquirySelectAllByField for equality-only filters, especially when returning a projection. Keep InquirySelectOneByKey as the explicit key lookup. InquirySelectAll remains the unfiltered form. This is a documented division of capabilities, not a claim that all forms are interchangeable.

The projection restriction on predicate selects is accepted for 1.0. They return the store entity, not an InquiryProjection DTO. To filter a projection beyond field equality, write parameterized SQL with an explicitly mapped result, or read entities and map them in application code when fetching all entity columns is acceptable. Do not move filtering into memory merely to bypass this restriction.

Supported capabilities

Declaration Result shape Filtering DISTINCT / ordering Pagination Eager relations
InquirySelectAll Entity or projection; buffered list or stream No method predicates Both Ordered offset, buffered; optional total No
InquirySelectAllByField Entity or projection; buffered list or stream Listed fields joined by equality AND Both Ordered offset, buffered; optional total No
InquirySelectAllByPredicate + InquiryWhere Entity only; buffered list or stream Comparisons, groups, optional criteria Both Ordered offset, buffered; optional total No
InquirySelectOneByKey Nullable entity Primary key Not configurable None No
InquiryKeysetPage InquiryPage of the entity and cursor Cursor seek, not arbitrary InquiryWhere filters Key-field order and one direction; no DISTINCT option Keyset only No
InquirySelectAllEager / InquirySelectOneByKeyEager Entity stream / nullable entity All parents / primary key Not configurable None Declared relationships

For offset paging, parameters end with int offset, int limit, then CancellationToken. Supply a stable OrderBy including a unique tie-breaker. InquiryPagedResult adds a total; DISTINCT with that result shape is rejected. A list-returning offset query may use DISTINCT, subject to the provider's SQL rules.

Keyset ordering fields must be non-nullable in the database and include a unique tie-breaker. Eager methods are separate operations, not an Include modifier on arbitrary filtered/projection queries. Runtime ContextKey filters in an eager tree are rejected. See Eager loading and Pagination for their further constraints.

Current declarations

These snippets use Inquiry.Stores, System.Collections.Generic, System.Threading, and System.Threading.Tasks. Product has an integer ProductID key, nullable integer CategoryID, string ProductName, and decimal UnitPrice. ProductSummary is an InquiryProjection of Product.

A simple entity filter uses explicit field names. Renaming FindByCategoryAsync does not change SQL:

[InquirySelectAllByPredicate]
[InquiryWhere(nameof(Product.CategoryID))]
public partial Task<IReadOnlyList<Product>> FindByCategoryAsync(
    int? categoryId, CancellationToken ct = default);

For an equality-only projection, use the field form:

[InquirySelectAllByField(nameof(Product.CategoryID))]
public partial Task<IReadOnlyList<ProductSummary>> SummariesAsync(
    int? categoryId, CancellationToken ct = default);

An ordered offset query keeps values and paging parameters separate:

[InquirySelectAllByPredicate(
    OrderBy = nameof(Product.ProductName) + " ASC, " + nameof(Product.ProductID) + " ASC",
    Paged = true)]
[InquiryWhere(nameof(Product.CategoryID))]
public partial Task<IReadOnlyList<Product>> PageByCategoryAsync(
    int? categoryId, int offset, int limit, CancellationToken ct = default);

The compound condition below means category matches AND either price is at least the minimum or the name matches the supplied LIKE pattern:

[InquirySelectAllByPredicate]
[InquiryWhere(nameof(Product.CategoryID))]
[InquiryWhere(nameof(Product.UnitPrice), Compare.GreaterThanOrEqual, OpenGroups = 1)]
[InquiryWhere(nameof(Product.ProductName), Compare.Like, Or = true, CloseGroups = 1)]
public partial Task<IReadOnlyList<Product>> SearchAsync(
    int? categoryId, decimal minimumPrice, string namePattern, CancellationToken ct = default);

Binding remains positional. The first criterion consumes categoryId, the next minimumPrice, and the last namePattern. nameof protects mapped field names, not parameter binding. Between consumes two parameters, IN consumes a collection, and IS NULL consumes none. Do not reorder criteria independently of their parameters. Explicit parameter binding is separate work, not a feature of this convention.

Comparison with a proposed unified attribute

The following is a proposal only. InquirySelect does not exist in the current packages:

// Proposal only. Do not paste into a current consumer.
[InquirySelect]
[InquiryWhere(nameof(Product.CategoryID))]
public partial Task<IReadOnlyList<Product>> FindByCategoryAsync(
    int? categoryId, CancellationToken ct = default);
Query Current form Proposed unified form Decision
Simple filter ByPredicate + Where, or ByField for equality Select + Where Keep explicit existing forms; no call-site change needed.
Compound condition ByPredicate + grouped Where criteria Select + the same criteria A rename does not make grouping or binding clearer.
Projection SelectAll / ByField returns a mapped DTO Select returning a DTO with Where Requires new projection/predicate support, not just a rename.
Ordering / offset OrderBy and Paged on supported attributes Same options on Select Shared spelling saves little while requiring migration and diagnostics changes.
Keyset / eager Separate operations with distinct results and lifetimes Would need additional modes and restrictions Keep their explicit declarations.

A future replacement would need equivalent result shapes, diagnostics, a deprecation period, and a mechanical migration before removing old forms. None is required now: existing attributes remain supported and no obsolete aliases are introduced. Moving a field-less ByField declaration to explicit nameof arguments is optional; preserve its field order and parameter types. This decision does not change mutations. Table-wide deletion still requires the explicit InquiryDeleteAll or InquiryHardDeleteAll declaration.

When to write SQL

Use attributes for fixed queries whose complete condition and parameter order a reviewer can read at the declaration. A single visible AND/OR group can be reasonable, as in SearchAsync above. When nested groups, negation, or reusable specifications require tracing counters across declarations to understand the query, prefer handwritten parameterized SQL in a named application method.

For example, keep the compound condition directly visible in SQL once it grows beyond that boundary. Use the provider's quoting and parameterized Inquiry SQL APIs; do not concatenate user values or user-selected identifiers. This is a readability choice, not an arbitrary limit enforced by the parser.

Neither retained nor proposed attributes provide runtime LINQ or arbitrary user-selected composition. Use a finite set of named generated queries for known shapes, or explicit SQL for a shape that needs joins, CTEs, subqueries, or unsupported projection predicates. Review that SQL and its materializer as part of the application contract.

See Projections, Ad-hoc DTOs, and CRUD.