Chapter 19 · SQL Server and Azure SQL Provider Deep Dive

Temporal Tables, JSON Columns, Sparse/Provider Features, and Query Translation

Use SQL Server temporal tables and EF Core 10 JSON mapping as observable provider features, including history-table lifecycle, point-in-time queries, native-json prerequisites, partial JSON updates, sparse-column tradeoffs, and version-sensitive migration consequences.

Advanced160–210 minutestemporal + JSON 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

Configure SQL Server temporal tables and explain current/history table lifecycle and UTC period semantics.

02

Query point-in-time/history data with TemporalAsOf/TemporalAll and interpret the generated FOR SYSTEM_TIME SQL.

03

Map EF Core 10 complex values to JSON and distinguish SQL Server 2025/Azure SQL native json from older nvarchar storage.

04

Observe JSON member translation and partial update behavior without claiming one SQL shape across every compatibility level.

05

Use sparse columns only when their null-heavy storage tradeoff is justified and supported by the SQL Server schema.

06

Recognize migration risks when compatibility/provider changes alter JSON column type or temporal metadata.

1. The problem: history and structured data are database features, not serialization tricks

ServiceHub now needs two SQL Server-specific capabilities: reconstruct a work order as it existed at an earlier time, and persist structured routing/contact details without exploding every nested value into standalone columns. SQL Server temporal tables and EF Core 10 JSON mapping can solve those problems, but they have versioned engine behavior, migration consequences, and query translations that differ from SQLite and older SQL Server versions.

Freeze the server assumptions first

For native SQL Server json in the EF Core 10 SQL Server provider, use Azure SQL or SQL Server 2025 with compatibility level 170+. If you target an older SQL Server/compatibility level, EF can still map JSON into textual storage. Do not copy SQL Server 2025 DDL into a SQL Server 2019 production environment.

2. Temporal tables externalize row history to SQL Server

A system-versioned temporal table maintains a current table plus a history table. SQL Server generates two period columns and copies old row versions into history on UPDATE/DELETE. EF Core can create/convert temporal tables with migrations and exposes temporal query operators. In the EF Core 10 baseline, period columns are normally shadow properties; mapping period columns to CLR properties is an EF Core 11 capability and is intentionally not used here.

csharp · temporal mapping for the SQL Server provider lab
modelBuilder.Entity<SqlServerWorkOrder>()    .ToTable("work_orders", table => table.IsTemporal(temporal =>    {        temporal.HasPeriodStart("ValidFrom");        temporal.HasPeriodEnd("ValidTo");        temporal.UseHistoryTable("work_order_history");    }));
sql · representative SQL Server table shape
CREATE TABLE [work_orders] (    ...,    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,    [ValidTo]   datetime2 GENERATED ALWAYS AS ROW END NOT NULL,    PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo])) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[work_order_history]));

SQL Server generates temporal period values in UTC. A history table is not an application audit log: it records row versions, not authenticated user intent, request IDs, or business reasons. Keep those concerns separate.

3. Query history with temporal operators, not handcrafted date predicates

TemporalAsOf returns the row version active at a UTC instant. TemporalAll returns all current/history versions. Other operators express interval semantics. Project the period shadow values to make the returned version boundaries observable.

csharp · point-in-time work order query
var instant = DateTime.UtcNow.AddMinutes(-30);var snapshot = await db.SqlServerWorkOrders    .TemporalAsOf(instant)    .Where(w => w.WorkOrderNumber == number)    .Select(w => new    {        w.WorkOrderNumber,        w.CustomerName,        ValidFrom = EF.Property<DateTime>(w, "ValidFrom"),        ValidTo = EF.Property<DateTime>(w, "ValidTo")    })    .SingleOrDefaultAsync(ct);
sql · representative translation
SELECT ...FROM [work_orders] FOR SYSTEM_TIME AS OF @__instant AS [w]WHERE [w].[WorkOrderNumber] = @__number

Use database-generated history for recovery/forensics only after defining retention, access controls, and storage growth. Temporal history can contain sensitive historical values that the current row no longer exposes.

4. EF Core 10 JSON mapping: same model concept, different SQL Server storage capability

EF Core 10 can map complex types to JSON. On SQL Server 2025 at compatibility level 170+ and on Azure SQL, the provider defaults to SQL Server's native json type. With lower compatibility or older servers, JSON is stored in a text column such as nvarchar(max). The C# complex-property API can look similar while storage, translation, and available indexing differ.

csharp · complex value mapped as one JSON column
public sealed record DispatchDetails(    string Region,    int ServiceTier,    string? EscalationCode,    DispatchAddress Address);public sealed record DispatchAddress(string City, string PostalCode);public sealed partial class SqlServerWorkOrder{    public DispatchDetails Dispatch { get; private set; } = null!;    public void ChangeDispatchRegion(string region)        => Dispatch = Dispatch with { Region = region };}modelBuilder.Entity<SqlServerWorkOrder>()    .ComplexProperty(w => w.Dispatch, complex => complex.ToJson());
csharp · SQL Server 2025 compatibility for native json
options.UseSqlServer(connectionString, sql =>    sql.UseCompatibilityLevel(170));
Target EF Core 10 JSON storage expectation Important note
SQL Server 2025 + compat 170 native json Review migration from existing nvarchar JSON columns
Azure SQL with UseAzureSql native json by default Cloud service behavior/version still needs verification
Older SQL Server / lower compat textual JSON storage JSON functions exist, but no native json column type

5. Query JSON members and inspect the generated function

A LINQ predicate over a mapped complex property becomes SQL Server JSON access. With the native SQL Server 2025 type, EF Core 10 can use the newer JSON_VALUE(... RETURNING ...) form for typed extraction. Text-backed JSON may require different casting/translation. Treat ToQueryString() as diagnostic text and verify the server plan separately.

csharp · query into the JSON document
var regional = await db.SqlServerWorkOrders    .Where(w => w.Dispatch.ServiceTier >= 3)    .Select(w => new { w.WorkOrderNumber, w.Dispatch.Region, w.Dispatch.Address.City })    .ToListAsync(ct);
sql · representative native-json predicate
SELECT [w].[WorkOrderNumber], ...FROM [work_orders] AS [w]WHERE JSON_VALUE([w].[Dispatch], '$.ServiceTier' RETURNING int) >= 3;
Feature-status boundary

EF Core 11 adds SQL Server JSON index modeling APIs. This EF Core 10 chapter does not use them. If production needs JSON indexing on EF Core 10, design the database-side index/computed-column strategy explicitly and manage it through reviewed SQL/migrations rather than pretending an EF10 JSON-index API exists.

6. Partial JSON updates are write-shape behavior you must observe

Changing one mapped JSON member does not necessarily mean EF rewrites the whole document. The SQL Server provider can generate partial-document updates (for text-backed JSON this commonly uses JSON_MODIFY; native-json SQL may differ by capability/version). Verify the actual command under the exact provider/server compatibility you deploy.

csharp · change one nested member
var workOrder = await db.SqlServerWorkOrders.SingleAsync(w => w.Id == id, ct);workOrder.ChangeDispatchRegion("south");await db.SaveChangesAsync(ct);
sql · representative text-backed partial update
UPDATE [work_orders]SET [Dispatch] = JSON_MODIFY([Dispatch], 'strict $.Region', JSON_VALUE(@p0, '$[0]'))WHERE [work_order_id] = @p1 AND [Version] = @p2;

Do not hard-code the representative SQL into application logic. The purpose is to prove that the provider generated the intended write shape and retained concurrency protection.

7. Sparse columns are a storage optimization for null-heavy SQL Server columns

SQL Server sparse columns reduce storage for NULL values at the cost of overhead for non-NULL values and have engine restrictions. They can be appropriate for rarely populated provider-specific fields. EF exposes IsSparse(); the decision still belongs to SQL Server schema/workload analysis.

csharp · optional sparse escalation note
modelBuilder.Entity<SqlServerWorkOrder>()    .Property(w => w.ProviderEscalationNote)    .HasMaxLength(1000)    .IsSparse();
sql · representative DDL
[ProviderEscalationNote] nvarchar(1000) SPARSE NULL

Do not mark every nullable column sparse. Measure null density, row shape, indexes, update patterns, and SQL Server sparse-column restrictions first.

8. Failure case: enabling compatibility 170 creates a non-trivial JSON migration

A team upgrades EF Core and switches the provider configuration from compatibility 160 to 170 without reviewing migrations. EF now sees mapped JSON columns as native json and scaffolds type changes from nvarchar(max). Even when SQL Server supports the conversion, it is a deployment/data compatibility event, not a harmless runtime toggle.

Repair: inspect the migration SQL, test representative documents, confirm dependencies/indexes/functions, stage the change, and keep compatibility below 170 or explicitly map nvarchar(max) if the organization is not ready to adopt native JSON. Record why the compatibility level is pinned.

9. Mandatory lab: build history, JSON and sparse evidence

  1. Use a disposable SQL Server 2025 Developer database. Record SELECT @@VERSION and DATABASEPROPERTYEX(DB_NAME(),'Version') plus the database compatibility level.
  2. Add temporal mapping to the provider-lab work_orders table and a complex Dispatch JSON property.
  3. Set compatibility 170 and scaffold a migration. Confirm temporal DDL and native json column type before applying.
  4. Insert one work order, update it twice, then run TemporalAll() and print ValidFrom/ValidTo.
  5. Query Dispatch.Region, capture ToQueryString(), and inspect the real command/log.
  6. Update only Dispatch.Region; inspect whether the provider emits a partial-document update on your exact server/provider build.
  7. Add one sparse nullable lab column and verify sys.columns.is_sparse.
  8. Repeat the JSON migration-generation step with compatibility 160 in a fresh throwaway database and compare DDL; do not infer behavior from memory.
Reset

Drop the disposable SQL Server lab database after saving the migration/SQL evidence needed for your notes. Temporal history can retain deleted sensitive values, so do not leave real data in a lab history table.

10. Production judgment and bridge

Temporal tables are appropriate when database-managed row history is part of the requirement and retention/security are designed. JSON is appropriate when an aggregate substructure belongs together and query/update needs fit SQL Server's JSON capabilities; native JSON requires SQL Server 2025/Azure SQL capability and careful migration review. Sparse columns are physical storage choices, not object-model decorations. The next lesson moves to Azure SQL resiliency, where transient-fault classification and retry scope matter more than any single mapping API.

Check your understanding

  1. What does a temporal history table record?
  2. Where are temporal period columns in the EF Core 10 baseline?
  3. When does EF Core 10 default SQL Server JSON to the native json type?
  4. Why must compatibility-level changes be migration-reviewed?
  5. Does EF Core 10 provide SQL Server JSON index modeling?
  6. When is IsSparse appropriate?
Review the answers

1. Database row versions over time; it does not automatically record application user intent or business reason.

2. Normally shadow properties; CLR-mapped period properties are an EF Core 11 feature.

3. With Azure SQL or SQL Server 2025/compatibility level 170+.

4. They can change storage types and translations, including existing JSON columns, which is a real schema/data deployment event.

5. No. SQL Server JSON-index modeling is an EF Core 11 capability.

6. Only after SQL Server workload/storage analysis shows a null-heavy column benefits and engine restrictions are acceptable.

Authoritative references

Temporal and JSON behavior changes with EF, SQL Server, Azure SQL and compatibility levels; verify all four before production rollout.

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