Chapter 19 · SQL Server and Azure SQL Provider Deep Dive

Identity, Sequences, rowversion, Computed Columns, Collations, and SQL Server Type Mapping

Move ServiceHub from the portable SQLite baseline to a deliberately isolated SQL Server provider lab and make generated values, rowversion concurrency, type facets, computed/default values, and collation semantics visible in the model, migrations, commands, and database.

Advanced160–210 minutesSQL Server generated-value 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

Explain which generated-value behaviors come from EF Core, the SQL Server provider, and SQL Server itself.

02

Choose between IDENTITY, explicit sequences, and HiLo only after considering key semantics, round trips, and migration shape.

03

Map SQL Server rowversion as a provider-generated concurrency token without replacing the course portable Revision token silently.

04

Configure Unicode, length, decimal/date-time precision, computed/default columns, and collations as storage/query semantics—not C# decoration.

05

Inspect model metadata, migration SQL, generated DML, and failure evidence before calling a SQL Server mapping correct.

06

Keep the provider lab isolated so Chapter 01–18 SQLite migrations and portable domain behavior remain reproducible.

1. The problem: the same C# model does not mean the same database behavior

Through Chapter 18, ServiceHub deliberately used SQLite as the mandatory baseline and an application-managed Guid Revision token. Moving to SQL Server introduces store behaviors that did not exist in that lab: IDENTITY, sequences, HiLo allocation, rowversion, persisted computed columns, SQL Server collations, and richer type facets. Those are not cosmetic provider switches. They change generated DDL, write round trips, concurrency predicates, value precision, and sometimes query plans.

Do not mutate the portable model in place

Create a disposable ServiceHub.SqlServerLab context/model for this chapter. The course-wide WorkOrder and its application-managed Revision remain valid portable examples; the SQL Server lab adds a provider-specific SqlServerWorkOrder with byte[] Version so rowversion semantics are explicit rather than hidden behind conditional mapping.

Term Mechanism What to verify
IDENTITY SQL Server generates a numeric value on INSERT Migration DDL, returned key, explicit-value behavior
Sequence Independent database object returns numbers Sequence DDL, gaps, allocation policy
HiLo EF obtains a high value from a sequence and allocates lows client-side Sequence calls versus inserted rows
rowversion SQL Server-generated 8-byte version changes on row update Concurrency WHERE predicate and returned version
Collation Database rules for text comparison/sort Case/accent behavior and index compatibility

2. Reproduce the provider boundary with a free local SQL Server path

Use SQL Server 2025 Developer edition locally (Windows installation or a supported container) for the mandatory provider lab. Developer edition is free for development/test but not a production license. Azure SQL is an optional cloud comparison, not a requirement. Keep the connection string outside source control.

bash · install the SQL Server provider and pin the toolchain
dotnet add src/ServiceHub.SqlServerLab package Microsoft.EntityFrameworkCore.SqlServer --version 10.0.11dotnet add src/ServiceHub.SqlServerLab package Microsoft.EntityFrameworkCore.Design --version 10.0.11dotnet tool update --local dotnet-ef --version 10.0.11dotnet --infodotnet ef --version
csharp · configure from environment, not source
var connectionString = configuration.GetConnectionString("SqlServer")    ?? Environment.GetEnvironmentVariable("SERVICEHUB_SQLSERVER")    ?? throw new InvalidOperationException("SQL Server connection is not configured.");services.AddDbContext<SqlServerServiceHubContext>(options =>    options.UseSqlServer(connectionString));

The NuGet 10.0.11 SQL Server provider depends on Microsoft.Data.SqlClient 6.1.6 or later. Record the actual server build and database compatibility level as part of every provider-specific lab because SQL Server and Azure SQL features do not move in lockstep.

3. IDENTITY is the default numeric generated-key strategy

For a numeric primary key configured as generated on add, the SQL Server provider normally creates an IDENTITY column. The value is generated by the database, so EF treats a new entity key as temporary until the INSERT returns the real value. This is different from SQLite implementation details even though the C# property may still be int Id.

csharp · explicit identity configuration
modelBuilder.Entity<SqlServerWorkOrder>(entity =>{    entity.ToTable("work_orders");    entity.HasKey(x => x.Id);    entity.Property(x => x.Id)        .HasColumnName("work_order_id")        .UseIdentityColumn(seed: 1, increment: 1);});
sql · representative migration DDL
CREATE TABLE [work_orders] (    [work_order_id] int NOT NULL IDENTITY(1,1),    ...    CONSTRAINT [PK_work_orders] PRIMARY KEY ([work_order_id]));

Gaps in an identity are normal: rollback, caching, failover, and deleted rows can leave holes. Do not use identity continuity as a business guarantee. ServiceHub continues to keep WorkOrderNumber as the externally meaningful business key.

4. Sequences and HiLo solve different allocation problems

A SQL Server sequence is a schema object independent of any table. You can use it directly as a default or let EF use a sequence-based HiLo strategy. HiLo reduces key-generation round trips by allocating a range of IDs in memory after obtaining a high value; the tradeoff is that unused ranges and gaps become normal and the strategy is provider/model infrastructure, not business numbering.

csharp · explicit sequence for a non-PK service number
modelBuilder.HasSequence<long>("service_ticket_seq")    .StartsAt(100_000)    .IncrementsBy(1);modelBuilder.Entity<SqlServerWorkOrder>()    .Property(x => x.InternalTicketNumber)    .HasDefaultValueSql("NEXT VALUE FOR [service_ticket_seq]");
csharp · alternative model-wide HiLo key generation
// Alternative lab model; do not combine this with IDENTITY for the same key.modelBuilder.UseHiLo("servicehub_hilo");
Production judgment

Choose IDENTITY for simple store-generated numeric keys. Consider HiLo when key-generation round trips are measurable and gaps are acceptable. Use explicit sequences when multiple tables/processes intentionally share a database sequence. None of these should generate a customer-facing identifier whose continuity has legal or business meaning.

5. rowversion is a SQL Server concurrency mechanism, not a portable byte[] timestamp

SQL Server rowversion is an automatically generated binary value that changes when the row is updated. EF maps it as both value-generated and a concurrency token. The provider then includes the original version in UPDATE/DELETE predicates, and a zero-row result becomes DbUpdateConcurrencyException. It is not a wall-clock timestamp and has no SQLite equivalent.

csharp · provider-specific rowversion mapping
public sealed class SqlServerWorkOrder{    public int Id { get; private set; }    public string WorkOrderNumber { get; private set; } = null!;    public string CustomerName { get; private set; } = null!;    public byte[] Version { get; private set; } = [];}modelBuilder.Entity<SqlServerWorkOrder>()    .Property(x => x.Version)    .IsRowVersion();
sql · representative concurrency-sensitive update
UPDATE [work_orders]SET [CustomerName] = @p0OUTPUT INSERTED.[Version]WHERE [work_order_id] = @p1 AND [Version] = @p2;

The returned rowversion replaces the tracked current/original value after a successful save. Keep the course portable Guid Revision token for SQLite and provider-neutral examples; use rowversion only inside the SQL Server boundary.

6. Type facets are data semantics: Unicode, precision, and time

CLR types do not specify SQL Server storage precision on their own. Define facets that follow the domain and query workload. For money-like values, precision/scale is a business rule; for strings, Unicode and length affect storage/indexability; for times, choose datetime2 or datetimeoffset deliberately and define precision where comparisons or external interchange depend on it.

csharp · explicit SQL Server facets
var e = modelBuilder.Entity<SqlServerWorkOrder>();e.Property(x => x.WorkOrderNumber).HasMaxLength(40).IsUnicode(false);e.Property(x => x.CustomerName).HasMaxLength(200).IsUnicode();e.Property(x => x.EstimatedCost).HasPrecision(12, 2);e.Property(x => x.OpenedUtc).HasColumnType("datetimeoffset(3)");e.Property(x => x.ClosedUtc).HasColumnType("datetime2(3)");
C# intent Typical SQL Server storage Risk if left implicit
string customer name nvarchar(n) Unbounded/max storage and weak index design
decimal cost decimal(p,s) Unexpected rounding/truncation contract
DateTime UTC instant datetime2(p) Kind is not persisted by SQL Server
DateTimeOffset instant+offset datetimeoffset(p) Larger storage; semantics differ from DateTime

7. Defaults and computed columns run in the database

A default is used when an INSERT does not provide a value. A computed column derives its value from other columns. These are store behaviors, so EF model configuration must match the actual DDL and value-generation metadata.

csharp · default and persisted computed value
e.Property(x => x.CreatedUtc)    .HasDefaultValueSql("SYSUTCDATETIME()");e.Property(x => x.SearchLabel)    .HasComputedColumnSql(        "CONCAT([WorkOrderNumber], N' · ', [CustomerName])",        stored: true);
sql · representative DDL
[CreatedUtc] datetime2 NOT NULL DEFAULT (SYSUTCDATETIME()),[SearchLabel] AS (CONCAT([WorkOrderNumber],N' · ',[CustomerName])) PERSISTED

Do not set a computed property as though it were ordinary writable state. Verify ValueGenerated metadata and inspect INSERT/UPDATE commands to ensure EF is not sending a value the store owns.

8. Collation decides text equality and ordering—and can decide index usage

SQL Server installations are commonly case-insensitive by default, while .NET string equality is case-sensitive. EF translates simple string equality to SQL equality and therefore inherits the database/column collation. If the domain requires a particular rule, configure it at database or column scope. Query-level EF.Functions.Collate is an escape hatch but may prevent use of an index whose collation differs.

csharp · column collation
e.Property(x => x.WorkOrderNumber)    .UseCollation("Latin1_General_100_BIN2");
csharp · query-level collation only when deliberately needed
var exact = await db.WorkOrders    .Where(w => EF.Functions.Collate(        w.CustomerName,        "SQL_Latin1_General_CP1_CS_AS") == customer)    .ToListAsync(ct);
Do not normalize text with ToLower() by reflex

A function around an indexed column can change sargability. If case rules are stable, encode them in the column/database collation and verify the actual execution plan.

9. Failure case: carrying explicit SQLite IDs into an IDENTITY table

A data-import script materializes old rows, copies their integer IDs, and calls Add. SQL Server rejects the insert because explicit values cannot normally be inserted into an identity column.

text · representative failure
SqlException: Cannot insert explicit value for identity column in table'work_orders' when IDENTITY_INSERT is set to OFF.

Repair: for ordinary application inserts, leave the identity key unset and preserve external/business identity in WorkOrderNumber. If a one-time migration must preserve physical IDs, design a reviewed data-migration step that scopes SET IDENTITY_INSERT ... ON/OFF, validates uniqueness/foreign keys, and never becomes normal application behavior.

10. Mandatory lab: inspect model → migration → INSERT → returned values

  1. Create a disposable SQL Server 2025 Developer database such as ServiceHubSqlServerLab; never point these exercises at production.
  2. Build SqlServerServiceHubContext with identity, explicit facets, a store default, a persisted computed column, and Version.IsRowVersion().
  3. Add a migration named SqlServerProviderBaseline and inspect both C# operations and generated SQL before applying it.
  4. Insert one work order without setting the identity, computed value, default timestamp, or rowversion.
  5. After SaveChangesAsync, print Id, CreatedUtc, SearchLabel, and the hex rowversion; inspect EF command logs for returned store values.
  6. Update the row from two contexts to re-confirm the concurrency predicate and conflict behavior learned in Chapter 13.
  7. Query sys.columns, sys.default_constraints, and sys.indexes to verify the live database rather than relying only on EF metadata.
Reset

Drop only the disposable ServiceHubSqlServerLab database or remove the local container volume used for this chapter. Keep Chapter 01–18 SQLite artifacts untouched.

11. Production judgment and bridge

SQL Server-specific value generation and type mapping are appropriate when the provider is a deliberate architectural dependency. Pin Microsoft EF packages at 10.0.11, record Microsoft.Data.SqlClient/server/compatibility levels, and review migrations for generated-value or collation changes. Rowversion protects a row from lost updates but does not replace aggregate/business concurrency policy. Collations and type facets can affect indexes and query results. The next lesson adds temporal history and JSON—features where server version and compatibility level become even more visible.

Check your understanding

  1. Why is rowversion not a portable replacement for ServiceHub Revision?
  2. When does SQL Server use IDENTITY for EF numeric generated keys?
  3. What problem can HiLo reduce?
  4. Why must decimal precision be explicit?
  5. Why can query-level COLLATE hurt performance?
  6. What should you inspect after adding provider-specific mappings?
Review the answers

1. It is a SQL Server-generated binary concurrency mechanism with no universal provider equivalent; the course Guid token remains the portable path.

2. By convention for numeric properties generated on add, unless another SQL Server value-generation strategy is configured.

3. Repeated database round trips solely to allocate numeric keys, at the cost of gaps and provider/model complexity.

4. The CLR decimal type does not encode the database precision/scale contract; rounding or overflow behavior is a data rule.

5. A collation different from the indexed column can make the existing index ineligible for that comparison.

6. Runtime model metadata, migration operations/SQL, generated DML and returned store values, plus the live SQL Server catalog.

Authoritative references

These features are provider- and server-sensitive; re-check the SQL Server provider and engine documentation before changing a production model.

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