EF Core 10 JSON Configuration¶
Verified by tests
JsonContextRegistryTests, JsonContextRegistryPriorityProfileTests — library CI run #31657041675 (2026-08-13)
Overview¶
EF Core 10 has native JSONB support for PostgreSQL. Whizbang stores perspective data, envelope metadata, and scope information in JSONB columns, and all of it must serialize with the same source-generated, AOT-compatible JSON configuration — including custom converters like the WhizbangId converters emitted by source generators.
The key pieces:
JsonContextRegistry(Whizbang.Core) — a global, cross-assembly registry of source-generatedJsonSerializerContextinstances and converters. Each assembly self-registers via[ModuleInitializer]at load time — no reflection, fully AOT-compatible.JsonContextRegistry.CreateCombinedOptions()— builds a singleJsonSerializerOptionsfrom every registered context (Core infrastructure types, EF Core types, and your application types).EFCoreJsonContext(Whizbang.Data.EFCore.Postgres) — registersEnvelopeMetadatawith the registry; exposesCreateCombinedOptions()as a convenience.
Turnkey Configuration (Recommended)¶
With the turnkey pattern, JSON configuration is fully automatic. The source-generated registration callback creates the NpgsqlDataSource with the combined JSON options already applied:
Turnkey Configuration
// One line — the generated module initializer handles JSON configuration
builder.Services.AddWhizbang()
.WithEFCore<MyDbContext>()
.WithDriver.Postgres;
Under the hood, the generated callback does the equivalent of:
Generated Registration (simplified)
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.ConfigureJsonOptions(JsonContextRegistry.CreateCombinedOptions());
dataSourceBuilder.EnableDynamicJson();
// dataSourceBuilder.UseVector() is added automatically when [VectorField] columns exist
:::updated{version="1.0.0"}
Earlier drafts of this page recommended registering JsonSerializerOptions in DI and avoiding NpgsqlDataSourceBuilder.ConfigureJsonOptions. Shipped behavior is the opposite: the turnkey registration configures JSON at the data source level via ConfigureJsonOptions(JsonContextRegistry.CreateCombinedOptions()) + EnableDynamicJson(). This is what guarantees that every JSONB read/write — EF Core queries, raw Npgsql commands, and the work-coordinator SQL surface — uses the identical converter set.
:::
Registering Your Own Types and Converters¶
Frameworks and applications contribute their JSON contexts to the global registry from a module initializer:
Registering a JsonSerializerContext
[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(MyLensDto))]
public partial class MyAppJsonContext : JsonSerializerContext {
[ModuleInitializer]
internal static void Initialize() {
JsonContextRegistry.RegisterContext(MyAppJsonContext.Default);
}
}
JsonContextRegistry also supports:
RegisterConverter(JsonConverter converter)— for converters that source generation can't express (e.g., WhizbangId converters); instances are created at compile time by source generators- Priority + profile overloads (
RegisterContext(resolver, priority, profile)) — infrastructure types from Core take precedence over application types; equal priorities preserve registration order
In practice you rarely write this by hand — the Whizbang source generators emit and register the contexts for your message and perspective types automatically.
Example: Perspective Row Storage¶
Perspective rows store your read-model DTOs in JSONB columns using the fixed PerspectiveRow<TModel> shape:
Example: Perspective Row Storage
public class PerspectiveRow<TModel> where TModel : class {
public required Guid Id { get; init; }
public required TModel Data { get; set; } // JSONB
public required PerspectiveMetadata Metadata { get; set; } // JSONB
public required PerspectiveScope Scope { get; set; } // JSONB
public required DateTime CreatedAt { get; init; }
public required DateTime UpdatedAt { get; set; }
public required int Version { get; set; }
}
Because the data source carries the combined options, EF Core automatically:
- Serializes
TModelto JSONB using your registered contexts and converters - Applies custom converters (like WhizbangId converters) consistently on both reads and writes
- Stays AOT-compatible — no reflection-based serialization anywhere in the path
Why Data-Source-Level Configuration¶
- One converter set everywhere — EF Core, Dapper, and raw
NpgsqlCommandpaths all flow through the sameNpgsqlDataSource, so JSONB bytes are identical regardless of which layer wrote them - AOT-safe —
CreateCombinedOptions()composes only source-generatedIJsonTypeInfoResolvers; there is no runtime reflection fallback - Zero per-service wiring — the generated module initializer means consumers never hand-configure JSON for infrastructure types