Chapter 04 · Keys, Indexes, Property Semantics, Value Generation, and Shadow State

Generated Values, Identity/Sequences, Default Values, Computed Columns, and Temporary Keys

Trace temporary, application-generated, default, computed, identity, sequence, and store-generated values so SaveChanges behavior is predictable across providers.

Intermediate95–120 minutesgenerated-value + SaveChanges evidence labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

After identity and indexes, ServiceHub must decide who owns each value. Id may be database-generated; OpenedUtc is application-supplied; audit timestamps may use database defaults; computed columns may be read back; sequences may exist only on some engines. EF's ValueGenerated... metadata describes expectations—it does not invent a database generation mechanism for every type/provider.

01

Distinguish application-supplied values, temporary EF values, database defaults, computed values, identity/autoincrement, sequences, and HiLo.

02

Inspect IsTemporary, ValueGenerated, BeforeSaveBehavior, and AfterSaveBehavior metadata instead of assuming generated-value behavior.

03

Use SQLite-compatible defaults/computed columns in a disposable lab while keeping SQL Server/PostgreSQL sequence examples provider-gated.

04

Explain how store-generated values flow back into tracked entities after SaveChanges.

05

Diagnose the wrong assumption that ValueGeneratedOnAdd alone guarantees the database knows how to generate a value.

1. Generation has three separate questions

Question Example
Who chooses the value? Application constructor, EF temporary generator, database default/identity/computed expression
When is it chosen? Before insert, during insert, or after an update
How does EF learn the final value? Client already knows it, provider returns it, or EF issues provider-specific retrieval

Do not collapse these questions into one ValueGeneratedOnAdd() call. Provider conventions decide many behaviors, and a migration must create the necessary database mechanism.

2. Integer primary keys are commonly generated on add

For relational providers, a non-composite integer primary key is commonly configured for generation on add. In EF Core 10, the Microsoft SQLite provider configures eligible numeric generated-on-add primary keys with SQLite AUTOINCREMENT by convention; its provider documentation also explains how to disable that strategy and fall back to default ROWID behavior when value reuse is acceptable. SQL Server commonly uses IDENTITY; PostgreSQL can use identity/sequence-backed strategies. Similar EF metadata therefore maps to different physical mechanisms.

csharp · observe a temporary key entry
var workOrder = new WorkOrder(    "WO-2026-000201",    "Contoso Pump Station",    "Inspect cavitation alarm",    WorkOrderPriority.High,    DateTimeOffset.Parse("2026-08-27T08:00:00Z"));db.Add(workOrder);var idEntry = db.Entry(workOrder).Property(x => x.Id);Console.WriteLine($"Before save entity Id: {workOrder.Id}");Console.WriteLine($"Tracker current: {idEntry.CurrentValue}");Console.WriteLine($"Temporary: {idEntry.IsTemporary}");await db.SaveChangesAsync(ct);Console.WriteLine($"After save entity Id: {workOrder.Id}");Console.WriteLine($"Temporary: {idEntry.IsTemporary}");

Do not hard-code an expected temporary integer such as -2147482647; generator details are implementation/version dependent. The stable evidence is IsTemporary before/after persistence and the final key propagated to the entity.

3. Database defaults apply when the insert omits a value

Add a CreatedUtc infrastructure value to the EF model as a shadow DateTime property normalized to UTC. The SQLite provider specifically recommends DateTime rather than DateTimeOffset when comparison/ordering is required. In this lab, CURRENT_TIMESTAMP is the database expression, so EF should omit the column when the value is generated on add and the store should supply it.

csharp · SQLite default timestamp as shadow property
builder.Property<DateTime>("CreatedUtc")    .HasColumnName("created_utc")    .HasDefaultValueSql("CURRENT_TIMESTAMP")    .ValueGeneratedOnAdd();

The SQL fragment is database syntax. SQL Server, PostgreSQL, MySQL/MariaDB, and Oracle have different time functions/types/precision semantics. If cross-provider behavior matters, test each engine rather than copying a default expression.

csharp · read the returned shadow value after SaveChanges
await db.SaveChangesAsync(ct);var created = db.Entry(workOrder)    .Property<DateTime>("CreatedUtc")    .CurrentValue;Console.WriteLine($"CreatedUtc from store: {created:O}");

4. Computed columns are generated from other stored values

A computed value differs from a default. A default is normally chosen when a row is inserted without a value. A computed column derives its value from an expression and may be virtual or stored depending on the database.

csharp · SQLite computed shadow column
builder.Property<string>("SearchLabel")    .HasColumnName("search_label")    .HasComputedColumnSql(        "customer_name || ' | ' || summary",        stored: true);

This is intentionally SQLite-specific SQL. It gives the chapter a reproducible local observation path without pretending the expression is portable. If the production provider changes, the migration must use that provider's expression and supported computed/generated-column semantics.

5. ValueGeneratedOnAdd/OnAddOrUpdate describe expectations, not magic

csharp · explicit generation metadata
builder.Property<DateTime>("CreatedUtc")    .ValueGeneratedOnAdd();builder.Property<string>("SearchLabel")    .ValueGeneratedOnAddOrUpdate();

Those calls tell EF when it should expect generated values. They do not, by themselves, define how a DateTimeOffset or string is generated in the database. Use a default, computed expression, provider-specific value-generation strategy, trigger, or another documented mechanism.

Failure mode

If model metadata says a value is generated but the database has no mechanism to generate it, inserts can store null/default values, fail constraints, or leave EF with incorrect assumptions. Always inspect migration DDL and real insert behavior.

6. Sequences and HiLo are provider capabilities, not SQLite baseline features

SQLite does not expose general database sequences like SQL Server/PostgreSQL/Oracle. Do not make a sequence mandatory in this course's local SQLite lab. The following is a provider-specific design example for an engine that supports sequences.

csharp · illustrative sequence configuration
// Provider-gated example; do not apply to the SQLite baseline.modelBuilder.HasSequence<long>("work_order_numbers")    .StartsAt(100000)    .IncrementsBy(10);// Provider-specific default SQL is then required, e.g. SQL Server NEXT VALUE FOR.// Verify exact syntax for the target provider before generating migrations.

HiLo is another strategy where blocks/high values are allocated so clients can generate several keys with fewer database round trips. The tradeoff includes gaps and provider-specific implementation. Do not choose it solely because it sounds “faster.”

7. Inspect generation and save-behavior metadata

csharp · metadata probe
var entity = db.Model.FindEntityType(typeof(WorkOrder))!;foreach (var property in entity.GetProperties()){    Console.WriteLine(property.Name);    Console.WriteLine($"  ValueGenerated: {property.ValueGenerated}");    Console.WriteLine($"  BeforeSave: {property.GetBeforeSaveBehavior()}");    Console.WriteLine($"  AfterSave:  {property.GetAfterSaveBehavior()}");    Console.WriteLine($"  Concurrency: {property.IsConcurrencyToken}");}

PropertySaveBehavior can be Save, Ignore, or Throw. These settings influence whether explicit/changed values are sent before or after saving. They are advanced model semantics; do not mutate them casually to force EF around a database-generated property.

8. Deliberately wrong approach: non-default generated key means “insert exactly this ID”

A frequent disconnected-data bug is to construct an object with a non-default value in a database-generated key and then expect EF to treat it as a brand-new row. EF key conventions and APIs can interpret non-default generated keys as existing identity, especially when attaching/updating graphs. The outcome can be an unexpected UPDATE, an insert conflict, or state that does not match developer intent.

csharp · dangerous disconnected pattern
// Simplified anti-pattern: incoming DTO carries a database-generated Id.var detached = MapIncomingWorkOrder(request); // detached.Id == 42db.Update(detached);       // Marks an existing identity for update-oriented persistence.await db.SaveChangesAsync(ct);

The repair is to define separate create/update contracts. New entities should not receive an arbitrary database-generated primary key from untrusted input. Existing updates should load/attach the intended identity explicitly and validate authorization/concurrency.

9. Hands-on lab: trace values before and after SaveChanges

  1. Keep WorkOrder.Id generated on add.
  2. Add shadow CreatedUtc with SQLite CURRENT_TIMESTAMP.
  3. Add the computed shadow SearchLabel using the SQLite concatenation expression.
  4. Generate and review the migration; verify which columns have defaults/generated SQL.
  5. Create a new work order and print Id, IsTemporary, CreatedUtc, and SearchLabel before saving.
  6. Call SaveChangesAsync once, then print the same values.
  7. Inspect the generated SQL/logs and SQLite schema.
  8. Change Summary, save again, and verify the computed label changes if the provider returns/reloads it as configured.
  9. Run the metadata save-behavior probe.
  10. Reset the disposable database after the experiment if you do not keep the generated columns for Chapter 04 continuation.

Verification checklist

  • No exact temporary-key number is asserted.
  • The migration, not only EF metadata, contains the actual database generation mechanism.
  • Default and computed generation are explained separately.
  • Sequence/HiLo examples are explicitly non-SQLite/provider-gated.
  • No create API accepts arbitrary database-generated IDs from untrusted input.

Check your understanding

  1. What does ValueGeneratedOnAdd guarantee about database DDL?
  2. Why should you inspect IsTemporary instead of hard-coding a temporary key value?
  3. How does a default differ from a computed column?
  4. Why is a sequence example provider-gated in this course?
  5. What can happen when a detached object carries a non-default generated primary key?
  6. What do BeforeSaveBehavior and AfterSaveBehavior describe?
Review the answers

By itself it does not create a universal generation mechanism; provider conventions/defaults/computed expressions/etc. must supply the actual strategy.

Temporary generator values are implementation details; IsTemporary is the semantic signal EF exposes.

A default supplies a value when inserting without one; a computed column derives a value from other state/expression and may be generated on insert/update.

SQLite has no general sequence facility equivalent to engines such as SQL Server/PostgreSQL/Oracle.

EF may treat that identity as existing depending on how the graph is attached/updated, producing unexpected updates or conflicts.

Whether values are saved, ignored, or rejected before the first save and after an entity already exists.

10. Production judgment and bridge

Make value ownership explicit. Application-generated identifiers simplify offline workflows but have their own ordering/index costs; database-generated keys centralize allocation but add store round trips/semantics; defaults and computed columns must be reviewed in migration DDL; sequences/HiLo are provider-specific tools.

Lesson 4 follows the shadow values introduced here. You will see where shadow data lives, how EF.Property reaches it in translated queries, why no-tracking entities cannot expose shadow state after materialization, and where indexer/property-bag patterns are useful or dangerous.

Authoritative references

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