Set-based mutations (UPDATE/DELETE by predicate)
Update or delete rows matching a WHERE predicate without loading entities — the
EF Core ExecuteUpdate/ExecuteDelete analog, with the SQL baked at compile time. Both
operations return the affected row count.
You write
public partial class CustomerStore : InquiryStore<Customer>
{
// Leading parameters map to SET columns by name. Trailing parameters
// bind the [InquiryWhere] criteria positionally.
[InquiryUpdate]
[InquiryWhere("LastSeen", Compare.LessThan)]
public partial Task<int> DeactivateStaleAsync(bool isActive, DateTime cutoff, CancellationToken ct = default);
[InquiryDelete]
[InquiryWhere("IsActive", Compare.Equal)]
[InquiryWhere("CreatedAt", Compare.LessThan)]
public partial Task<int> PurgeInactiveBeforeAsync(bool isActive, DateTime cutoff, CancellationToken ct = default);
}
For an update, Inquiry calculates how many trailing parameters the predicates consume, then maps every
leading parameter to a mutable entity property or column with the same name (case-insensitive). In the
example, isActive maps to Customer.IsActive; cutoff belongs to the predicate. This makes a
single-column partial update concise:
[InquiryUpdate]
[InquiryWhere(nameof(Member.Key))]
public partial Task<int> UpdateEmailAsync(
string email,
int key,
CancellationToken ct = default);
This emits the equivalent of UPDATE Member SET Email = @Email WHERE Key = @Key. Add more leading
parameters to update more columns. Each update parameter must have a unique name that resolves to its
target property or mapped column.
[InquiryWhere] works exactly as on
predicate selects: repeat it per criterion, criteria combine in declaration order
(AND by default, Or = true to join with OR), and parameters bind positionally —
Compare.Between / Compare.NotBetween consume two, Compare.In / Compare.NotIn one collection
(expanded at run time; an empty NotIn matches every row), IsNull/IsNotNull none. The full operator
set is the comparisons (Equal, NotEqual, GreaterThan, …), Like / NotLike (string fields), In
/ NotIn, Between / NotBetween, and the null checks.
Rules
- The method must return
Task<int>(rows affected). - At least one
[InquiryWhere]is required for parameter-inferred updates and predicate deletes.[InquiryUpdate]without a predicate remains the entity-by-key update. A targetless[InquiryDelete]is a compile error; use[InquiryDeleteAll]for an explicit table-wide delete. [InquiryUpdate]'s SET fields must be ordinary mutable columns: not the key, not database-generated, not the soft-delete indicator, not a concurrency token.- Concurrency-token entities are rejected for both operations (same rationale as batch mutations: a set-based statement cannot check per-row tokens).
Soft-delete entities
On a soft-delete entity, [InquiryDelete] emits the soft UPDATE form (sets the
indicator) and both operations compose the active-row filter into the WHERE — already-deleted
rows are never updated or re-deleted. [InquiryHardDelete] forces a literal
DELETE, matching the key-delete behavior.
Delete targeting
Use the narrowest form that expresses the intended target:
// One row by primary key.
[InquiryDelete]
public partial Task<bool> DeleteByKeyAsync(int key, CancellationToken ct = default);
// Every row matching a predicate.
[InquiryDelete]
[InquiryWhere(nameof(Member.Email))]
public partial Task<int> DeleteByEmailAsync(string email, CancellationToken ct = default);
// Every row whose key is in the supplied collection.
[InquiryDelete]
[InquiryWhere(nameof(Member.Key), Compare.In)]
public partial Task<int> DeleteByKeysAsync(IReadOnlyList<int> keys, CancellationToken ct = default);
// Every row in the table. The explicit attribute is required.
[InquiryDeleteAll]
public partial Task<int> DeleteAllAsync(CancellationToken ct = default);
[InquiryDeleteAll] performs the normal soft-delete update for a soft-delete entity. Use
[InquiryHardDeleteAll] to emit a literal table-wide DELETE. Write-enforced global filters still apply.
Transactions and timeouts
Like every generated method, set-based mutations participate in ambient Inquiry transactions and
honor InquiryOptions.DefaultCommandTimeout. A predicate mutation is a single statement, so it is
atomic on its own.