Chapter 06 · Complex Types, Owned Types, JSON Mapping, Conversions, Comparers, and Encapsulation

Map Structured Data to Relational JSON Columns and Reason About Partial Updates

Map ServiceHub structural data to relational JSON with EF Core 10, inspect provider SQL for nested queries/partial updates, and keep portability/index/schema-evolution boundaries explicit.

Intermediate105–135 minutesSQLite JSON query/update + provider comparison labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

Table-split complex values are transparent relational columns. Sometimes ServiceHub instead needs one cohesive document-shaped value—for example, integration-specific dispatch instructions that change together and contain nested contact/route data. EF Core 10 can map complex types to a relational JSON column with ToJson. The abstraction is provider-agnostic at the model level; the generated SQL and storage type are not.

01

Map a complex DispatchProfile to one JSON column using EF Core 10 ToJson.

02

Query scalar members inside a JSON complex value and inspect SQLite translation.

03

Observe SaveChanges partial JSON updates and distinguish them from whole-document replacement.

04

Explain EF Core 10 ExecuteUpdate support for relational JSON complex types and its owned-type limitation.

05

Compare SQLite text/JSON functions with SQL Server 2025 native json and provider-specific PostgreSQL/MySQL behavior.

06

Design JSON storage from query/index/update/validation needs rather than convenience alone.

1. JSON is a storage shape, not an escape from modeling

A JSON column can reduce column sprawl and preserve document locality, but constraints, indexing, query translation, migration compatibility, and partial-update behavior become provider-sensitive. The first decision is still the domain model; ToJson changes how a complex value is stored.

csharp · document-shaped complex value
public sealed record DispatchContact(    string Name,    string Phone);public sealed record DispatchProfile(    string AccessInstructions,    DispatchContact Contact,    List<string> RequiredTools);public sealed partial class WorkOrder{    public DispatchProfile DispatchProfile { get; private set; } =        new("No special access", new("Dispatch", "000000"), []);}

2. Map the complex value to one JSON column

csharp · EF Core 10 JSON complex mapping
builder.ComplexProperty(x => x.DispatchProfile, json =>{    json.ToJson("dispatch_profile");    json.ComplexProperty(x => x.Contact);    json.PrimitiveCollection(x => x.RequiredTools);});

Provider support matters. EF Core 10's complex JSON mapping is the model-level API. SQLite stores JSON in a text column and translates through SQLite JSON functions. SQL Server 2025/Azure SQL at compatibility level 170 can use the native json data type. Third-party PostgreSQL/MySQL/Oracle providers have independent version/translation matrices and must be verified before copying this model into their production stack.

3. Query into JSON and inspect SQLite SQL

csharp · LINQ over JSON members
var gated = db.WorkOrders    .Where(x => x.DispatchProfile.AccessInstructions.Contains("gate"))    .Select(x => new    {        x.WorkOrderNumber,        x.DispatchProfile.Contact.Name    });Console.WriteLine(gated.ToQueryString());var rows = await gated.ToListAsync(ct);

SQLite JSON translations may use json_extract, ->>, or related JSON functions/operators depending on shape and EF/provider version. The exact SQL is evidence to capture, not text to memorize.

sql · representative SQLite JSON predicate shape
SELECT "w"."work_order_number",       json_extract("w"."dispatch_profile", '$.Contact.Name')FROM "work_orders" AS "w"WHERE instr(    json_extract("w"."dispatch_profile", '$.AccessInstructions'),    'gate') > 0;

4. SaveChanges can patch one JSON path

EF's SQLite JSON support can translate a single nested change into a json_set update rather than blindly replacing every document byte. Verify generated command logs because patch granularity depends on change shape/provider capabilities.

csharp · replace immutable nested value
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReplaceDispatchProfile(    order.DispatchProfile with    {        Contact = order.DispatchProfile.Contact with { Phone = "+994-12-555-0101" }    });await db.SaveChangesAsync(ct);
sql · representative SQLite path update
UPDATE "work_orders"SET "dispatch_profile" = json_set(    "dispatch_profile",    '$.Contact.Phone',    json_extract(@p0, '$[0]'))WHERE "work_order_id" = @id  AND "revision" = @original_revisionRETURNING 1;

Chapter 04's Revision concurrency token remains part of the predicate. JSON storage does not remove concurrency concerns.

5. ExecuteUpdate support is an EF Core 10 capability—with an important boundary

EF Core 10 added ExecuteUpdateAsync support for relational JSON columns when the document is modeled as a complex type. The release documentation explicitly distinguishes this from owned JSON mapping.

csharp · bulk update a JSON complex member
await db.WorkOrders    .Where(x => x.Status == WorkOrderStatus.Queued)    .ExecuteUpdateAsync(setters => setters        .SetProperty(            x => x.DispatchProfile.AccessInstructions,            x => x.DispatchProfile.AccessInstructions + " | confirm access"), ct);

Because ExecuteUpdate is immediate set-based DML, it bypasses tracked-entity synchronization. Do not run it while assuming already-tracked objects will magically refresh. Chapter 12 covers those write semantics in depth.

6. Deliberately wrong claim: “JSON behaves the same on every relational provider”

Provider/database Storage/translation reality to verify
SQLite JSON stored in text; JSON functions/operators such as json_extract/json_set; no native JSON column type.
SQL Server 2025 / Azure SQL compat 170 EF Core 10 can default to SQL Server native json; translation uses SQL Server JSON capabilities.
Older SQL Server compatibility JSON commonly remains textual storage; compatibility level changes generated SQL possibilities.
PostgreSQL / MySQL / Oracle Database has JSON capabilities, but EF provider APIs/translations/version support are provider-maintainer contracts; re-check exact packages.

Portability means testing the model and workload on each supported provider, not assuming identical SQL, indexes, collation, migrations, or update granularity.

7. JSON indexing is a separate database-design decision

Frequent filtering on DispatchProfile.Contact.Phone may require a provider-specific generated column/expression/JSON index strategy. EF Core 11 adds more complex-path/SQL Server JSON-index APIs, but this EF10 course does not present those preview features as stable. For EF10, review provider/database indexes explicitly in migrations or database DDL and keep portability boundaries visible.

8. Schema evolution inside documents still needs migration thinking

A JSON column reduces relational DDL for internal shape changes, but old documents may not contain newly required members. Defaults in CLR constructors do not automatically rewrite historical JSON. Plan backward-compatible materialization, backfills, validation, and deployment ordering. Treat open provider/framework bugs as reasons to test representative old documents, not as folklore to ignore JSON entirely.

Safety

Before adding a required nested JSON member, seed at least one “old-shape” document in integration tests and verify read-modify-save behavior on the exact EF/provider patch you deploy.

9. Hands-on lab: measure the document boundary

  1. Add DispatchProfile as an EF Core 10 complex type mapped with ToJson to the disposable SQLite database.
  2. Generate/review migration SQL and inspect the stored column with the SQLite CLI.
  3. Query by nested contact/access values and capture ToQueryString().
  4. Modify only the phone number; capture EF command logs and verify whether SQLite uses a JSON path update.
  5. Replace the whole DispatchProfile and compare command shape.
  6. Run one ExecuteUpdateAsync against a nested JSON member and then clear/requery tracked state before verifying results.
  7. Create an older JSON document missing a newly added optional member; verify materialization behavior.
  8. Record the SQLite library/provider versions with the lab evidence.

Verification checklist

  • The model uses EF Core 10 complex JSON mapping, not a hand-written serialization converter for this structured document.
  • Generated SQL is captured for query and update.
  • Concurrency predicates remain visible.
  • Tracked objects are not assumed synchronized after ExecuteUpdate.
  • Provider-specific JSON behavior is labeled.

Check your understanding

  1. Why is JSON mapping not a replacement for domain modeling?
  2. How does SQLite physically store JSON?
  3. What function is commonly used for partial JSON updates on SQLite?
  4. What EF Core 10 restriction applies to ExecuteUpdate on relational JSON?
  5. Why must old JSON shapes be tested during schema evolution?
  6. What is special about SQL Server 2025 compatibility level 170?
Review the answers

The value/entity/ownership boundary still determines semantics; JSON only changes persistence shape.

As text, interpreted by SQLite JSON functions/operators.

json_set is used for path updates in supported shapes.

The EF10 release notes describe relational JSON ExecuteUpdate support for complex types, not owned JSON structures.

Historical documents do not gain new CLR-initialized members automatically; materialization/save behavior must be verified.

EF Core 10 can use SQL Server 2025 native json behavior/type when configured for that compatibility level.

10. Production judgment and bridge

Choose JSON when document locality and query patterns justify it. Keep query frequency, indexes, update granularity, document size, schema evolution, provider support, backup/inspection tooling, and portability in the decision. The next lesson maps single properties through value converters and explains when EF also needs a ValueComparer to snapshot and compare domain values correctly.

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