sql-server-data-access
SQL Server data access standards for .NET systems — T-SQL coding rules, stored procedure contracts, EF Core and Dapper coexistence per module recipe, expand/contract migrations, index design, SARGabil
Files2
View on GitHubSKILL.md.claude/skills/sql-server-data-access/SKILL.md
GitHubSQL Server Data Access
1. Where logic may live (from the profile)
| Module domain_logic | Business rules in SQL | SPs for writes | SPs for reads |
|---|---|---|---|
| domain-model | ❌ (constraints as last line of defense only) | ❌ (except bulk/maintenance) | ✅ |
| transaction-script | ✅ if covered by integration tests | ✅ | ✅ |
| table-module | ✅ natural home | ✅ | ✅ |
2. T-SQL rules
SET NOCOUNT ON; SET XACT_ABORT ON;at the top of every procedure.- Schema-qualify every object; explicit column lists; no
SELECT *outsideEXISTS. BEGIN TRY / BEGIN TRAN … COMMIT / END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; THROW; END CATCH.- Types:
datetime2(3)(ordatetimeoffsetwhen offset matters),datefor dates,decimal(19,4)money,bitflags,uniqueidentifierwith sequential generation (v7 from app) orbigintidentity, sizednvarchar(n). - SARGable predicates: no functions on indexed columns, no implicit conversions (match parameter types exactly),
date ranges as
>= @From AND < @ToExclusive. - Set-based over cursors/loops;
MERGEonly withHOLDLOCKand a reason — prefer explicitUPDATE+INSERT … WHERE NOT EXISTS. - No
NOLOCK. Use RCSI (database-level) for read/write contention. - Temp tables for multi-step processing with statistics needs; table variables only for tiny sets.
- Dynamic SQL only via
sp_executesqlwith parameters; whitelist identifiers withQUOTENAME.
3. Stored procedure contract
- Name
{schema}.{Entity}_{Verb}(e.g.reporting.SaftD406_GenerateSalesInvoices). - Inputs typed exactly as target columns; table-valued parameters for sets.
- One result shape per procedure (or documented multiple result sets in fixed order).
- Error signaling:
THROW 50000 + <code>with codes documented; the app maps codes toError. - Grant
EXECUTEto the module's DB role only.
4. EF Core + Dapper coexistence
- Writes via EF (domain-model) share the connection/transaction with Dapper when needed:
db.Database.GetDbConnection()+db.Database.CurrentTransaction?.GetDbTransaction(). - Reads: EF
AsNoTracking().Select()for simple shapes; Dapper for complex SQL. Never materialize entities to map to DTOs. - Compiled queries or
EF.CompileAsyncQueryfor hot paths;AsSplitQuerywhen cartesian explosion appears. - Command timeout set per use case for reports; never globally huge.
5. Migrations — expand / migrate / contract
- Expand: add nullable column / new table / new index
ONLINE = ON(edition permitting); deploy app writing both. - Migrate: backfill in batches (
TOP (5000)loops withWAITFOR DELAYif needed), verifiable counts. - Contract: make NOT NULL / drop old column in a later release after all readers moved.
- Large-table changes: estimate row count and lock impact; schedule; avoid size-of-data operations in peak hours.
- Every migration idempotent (
IF NOT EXISTS) for DbUp/sqlproj; EF migrations reviewed as SQL (dotnet ef migrations script).
6. Indexing
- Clustered key: narrow, static, ever-increasing when possible.
- Nonclustered for each important read predicate: equality columns first, then range,
INCLUDEfor covered columns. - Filtered indexes for status-based queues (
WHERE Status = 'Pending'). - Every new index states its write cost and the queries it serves. Remove unused ones (sys.dm_db_index_usage_stats) via ADR-less maintenance plan.
7. Performance investigation procedure
Read references/performance-playbook.md. Summary: reproduce with real parameters → actual plan +
STATISTICS IO, TIME → look for scans with high reads, key lookups, implicit conversions, bad
estimates → Query Store for regressions/parameter sniffing → fix query first, index second, hints last.
8. Concurrency
- Default
READ COMMITTED+ RCSI enabled at DB level. - Optimistic concurrency via
rowversionon aggregate roots; map conflicts to a domain-level error. - Sequences needing no gaps (fiscal numbering): dedicated row per series updated with
UPDLOCK, HOLDLOCKin the same transaction as the insert; document serialization impact. - Deadlocks: capture from
system_healthXE; fix access order and indexing before retry logic; retries only for idempotent operations.