Chapter 19 · SQL Server and Azure SQL Provider Deep Dive

Provider-Specific SQL Inspection: When EF Abstractions Leak and How to Keep Them Intentional

Read SQL Server-specific translations for pagination, text/date operations, JSON, APPLY, set operations, null semantics, and DML, then isolate provider extensions behind an intentional boundary so portability failures are visible in tests rather than production.

Advanced160–210 minutestranslation-boundary labEF Core 10.0.11 · SQL Server provider 10.0.11 · .NET 10.0.11 · SDK 10.0.400SQL Server 2025 Developer free local path · Azure SQL optionalProvider deep-dive reviewed: August 2026

Learning outcomes

01

Inspect SQL Server translations for pagination, string/date operations, APPLY, JSON, set operations, null behavior, and set-based DML.

02

Distinguish stable relational LINQ intent from SQL Server-specific extensions/functions and compatibility-level behavior.

03

Use ToQueryString, command logs, query tags, and SQL Server plans as complementary evidence rather than interchangeable tools.

04

Create a dedicated provider boundary for SQL Server-specific queries so portability tradeoffs are visible and testable.

05

Reproduce a portability failure by running a SQL Server-only function against the SQLite baseline and repair the architecture.

06

Decide intentionally when provider-specific SQL is the correct engineering choice instead of treating it as an ORM failure.

1. The problem: “portable LINQ” ends where database semantics begin

EF Core gives ServiceHub one LINQ programming model, but providers translate expression trees into different SQL dialects and capabilities. SQL Server has APPLY, collations, temporal operators, full-text search, OFFSET/FETCH, SQL Server date functions, native JSON on newer compatibility levels, and provider-specific DML shapes. The goal is not to avoid those features; it is to keep the dependency intentional and observable.

Provider boundary rule

A SQL Server-specific query may be the best solution. Put it behind a name/API that says “SQL Server,” keep a provider-specific integration test, and preserve a simpler portable path only where the product actually needs portability.

2. Pagination becomes OFFSET/FETCH and still needs deterministic ordering

Skip/Take with ordering normally becomes SQL Server OFFSET ... FETCH. The relational cost of deep offset pagination remains: SQL Server still has to navigate/order rows before discarding earlier ones. Chapter 08 keyset-pagination guidance still applies.

csharp · offset page
var page = await db.SqlServerWorkOrders    .OrderByDescending(w => w.OpenedUtc)    .ThenByDescending(w => w.Id)    .Skip(offset)    .Take(pageSize)    .Select(w => new { w.Id, w.WorkOrderNumber, w.OpenedUtc })    .ToListAsync(ct);
sql · representative SQL Server shape
ORDER BY [w].[OpenedUtc] DESC, [w].[work_order_id] DESCOFFSET @__offset ROWS FETCH NEXT @__pageSize ROWS ONLY

3. String and collation semantics leak through equality and LIKE

SQL Server comparisons follow database/column collation. EF.Functions.Like produces SQL LIKE; EF.Functions.Collate emits COLLATE. A C# string comparison therefore does not magically retain .NET ordinal semantics. Query-level collation can also change index eligibility.

csharp · provider-visible text predicate
var results = await db.SqlServerWorkOrders    .Where(w => EF.Functions.Like(w.CustomerName, prefix + "%"))    .Where(w => EF.Functions.Collate(        w.WorkOrderNumber,        "Latin1_General_100_BIN2") == exactNumber)    .ToListAsync(ct);
sql · representative fragment
WHERE [w].[CustomerName] LIKE @__pattern  AND [w].[WorkOrderNumber] COLLATE Latin1_General_100_BIN2 = @__exactNumber

4. Date/time members translate to SQL Server functions

Provider function mappings are versioned. For example, DateTime.UtcNow maps to SQL Server UTC time functions, and EF Core 10 added translations for microsecond/nanosecond members. Use these translations only when the database-side clock/precision is the intended semantic.

csharp · database-evaluated age predicate
var stale = await db.SqlServerWorkOrders    .Where(w => w.OpenedUtc < DateTimeOffset.UtcNow.AddHours(-4))    .ToListAsync(ct);

If the domain needs one authoritative “now” across multiple operations, capture it once or use a transaction/database expression deliberately. Repeated server-function calls and application clocks are not automatically identical.

5. Correlated collection selectors can become CROSS APPLY or OUTER APPLY

Some LINQ shapes reference the outer element inside a collection selector rather than a simple join predicate. SQL Server can translate these with CROSS APPLY or OUTER APPLY. SQLite does not support APPLY, which is why Chapter 09 treated this translation as provider-dependent.

csharp · correlated projection
var labels = await db.SqlServerWorkOrders    .SelectMany(        w => db.Technicians.Select(t => new        {            w.Id,            Label = w.WorkOrderNumber + ":" + t.DisplayName        }))    .Take(20)    .ToListAsync(ct);
sql · representative shape
SELECT TOP(@__p_0) ...FROM [work_orders] AS [w]CROSS APPLY (    SELECT [w].[WorkOrderNumber] + N':' + [t].[DisplayName] AS [Label]    FROM [technicians] AS [t]) AS [x]

Do not use APPLY because it is “advanced.” Inspect cardinality and plan shape; a simpler join or projection may be clearer and cheaper.

6. JSON translation depends on storage and compatibility

EF Core 10 can translate member access inside mapped JSON complex values. On SQL Server 2025/compatibility 170 native JSON, typed JSON_VALUE(... RETURNING ...) forms become available. Text-backed JSON on older compatibility uses different expressions/casts. JSON indexes modeled by EF belong to EF Core 11, not this baseline.

csharp · JSON member filter
var priorityDispatch = await db.SqlServerWorkOrders    .Where(w => w.Dispatch.ServiceTier >= 3)    .Select(w => new { w.Id, w.Dispatch.Region, w.Dispatch.Address.City })    .ToListAsync(ct);
sql · native-json representative predicate
WHERE JSON_VALUE([w].[Dispatch], '$.ServiceTier' RETURNING int) >= 3

7. Set operations preserve SQL bag/set semantics

Concat maps to UNION ALL semantics, while Union removes duplicates. Intersect and Except rely on SQL Server set semantics, type coercion and collation. Normalize compatible projection shapes before combining them, as taught in Chapter 09.

csharp · two compatible read-model sources
var urgent = db.SqlServerWorkOrders    .Where(w => w.Priority >= 4)    .Select(w => new { w.Id, w.WorkOrderNumber });var escalated = db.SqlServerWorkOrders    .Where(w => w.ProviderEscalationNote != null)    .Select(w => new { w.Id, w.WorkOrderNumber });var combined = await urgent.Union(escalated).ToListAsync(ct);

Use ToQueryString() to confirm UNION versus UNION ALL, then inspect the plan if duplicate elimination becomes expensive.

8. Set-based DML exposes SQL Server update/delete shape

ExecuteUpdate and ExecuteDelete bypass normal change-tracker synchronization and execute immediately, as Chapter 12 established. SQL Server typically emits direct set-based DML. Provider capability still determines which setters/joins can translate.

csharp · close stale lab work orders in one command
var affected = await db.SqlServerWorkOrders    .Where(w => w.Status == WorkOrderStatus.Open && w.OpenedUtc < cutoff)    .ExecuteUpdateAsync(setters => setters        .SetProperty(w => w.Status, WorkOrderStatus.Closed), ct);
sql · representative SQL Server DML shape
UPDATE [w]SET [w].[Status] = @__ClosedFROM [work_orders] AS [w]WHERE [w].[Status] = @__Open AND [w].[OpenedUtc] < @__cutoff;

If rowversion business concurrency must be enforced for a bulk update, include the token predicate yourself and inspect affected; ExecuteUpdate does not automatically perform tracked-entity concurrency conflict detection for a whole set.

9. Failure case: SQL Server full-text extension leaks into the SQLite path

A shared repository method adds EF.Functions.FreeText because it works well on SQL Server. The same method is then exercised by the course SQLite test provider. The SQLite provider cannot translate the SQL Server-specific function, so the “portable repository” was never portable.

csharp · provider-specific query—name it accordingly
public IQueryable<SqlServerWorkOrder> SearchWithSqlServerFullText(    SqlServerServiceHubContext db,    string term){    return db.SqlServerWorkOrders        .Where(w => EF.Functions.FreeText(w.CustomerName, term));}

Repair: move the method behind an explicitly SQL Server-specific query service/provider adapter and keep an integration test against SQL Server. Provide a separate portable fallback only if the product requirement needs one; do not silently degrade a full-text search into a different semantic.

10. Evidence stack: ToQueryString is necessary but not sufficient

For each provider-specific query, collect evidence at multiple layers:

Layer Evidence Question answered
LINQ/expression review + tests Is application intent correct?
EF translation ToQueryString + command logs What SQL/parameters did provider generate?
Database actual plan, reads, waits How did SQL Server execute it?
Application trace Activity/log duration Where did end-to-end time go?
Cross-provider test SQL Server + SQLite/PostgreSQL where required Is portability real or accidental?

Never conclude that “EF is slow” from SQL text alone. Never conclude that a provider-specific query is wrong solely because another provider cannot translate it. The architecture question is whether the dependency is intentional, isolated, tested, and operationally understood.

11. Mandatory lab: build a SQL Server translation notebook

  1. Use the disposable SQL Server provider database and seed the same deterministic ServiceHub rows used in earlier chapters.
  2. For one pagination, collation, date, APPLY, JSON, set-operation, and ExecuteUpdate query, save the LINQ expression and ToQueryString() output.
  3. Run each query with a TagWith("ServiceHub:SqlServerDeepDive:...") tag and capture the real command log.
  4. For two performance-relevant queries, capture actual SQL Server plans and logical reads; note parameter values/distribution without storing sensitive data.
  5. Run the SQL Server full-text example only after creating/validating the required full-text infrastructure; otherwise keep it as a translation-boundary observation.
  6. Execute the SQL Server-only method under the SQLite integration provider and record the translation failure as an expected architectural test.
  7. Document which methods stay in the portable query layer and which live in SqlServerServiceHubQueries.

12. Production judgment and bridge to Chapter 20

EF Core abstractions are most valuable when they make intent clear while keeping provider behavior inspectable. Use SQL Server-specific capabilities when they solve a measured product/database problem, but isolate them, pin provider compatibility, inspect SQL/plans, and test failure modes. Do not promise identical null/collation/JSON/function/DDL behavior across providers. Chapter 20 now applies the same discipline to SQLite, including its type, concurrency, DDL, locking and translation constraints.

Check your understanding

  1. What does Skip/Take typically become on SQL Server?
  2. Why is EF.Functions.Collate provider-visible?
  3. Why can APPLY be a portability boundary?
  4. What is the EF Core 10 limitation around SQL Server JSON indexes?
  5. Does ExecuteUpdate automatically synchronize tracked entities or raise tracked concurrency conflicts?
  6. When is a SQL Server-specific query acceptable?
Review the answers

1. An ordered OFFSET/FETCH query; deterministic ordering and deep-offset cost still matter.

2. It emits a database COLLATE operation whose semantics and index compatibility are defined by SQL Server.

3. SQL Server supports CROSS/OUTER APPLY for correlated collection selectors, while providers such as SQLite do not support APPLY.

4. EF Core 10 does not model SQL Server JSON indexes; that support starts in EF Core 11.

5. No. It executes immediately outside normal tracker synchronization; concurrency predicates/affected-row policy must be deliberate.

6. When the provider dependency is intentional, isolated, integration-tested, observable, and justified by product/workload requirements.

Authoritative references

SQL translation is versioned behavior. Re-check the provider function mapping and query documentation when upgrading EF Core or SQL Server compatibility.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n