Skip to content

Entity Framework

Preview Value Objects Reviewed 2026-09-19 purview-dev/value-objects c-sharp csharp ddd domain-driven-design dotnet dto entity-framework immutable json roslyn scalar serialization single-case-union source-generator validation value-object value-objects zodsharp

Purview.ValueObjects value objects work well as Entity Framework property types, especially with JSON columns on SQL Server (json/jsonb) and Postgres (jsonb).

Scalar value objects serialize as their underlying primitive, so they store naturally in a JSON column. Use a JsonSerializerOptions that registers ScalarJsonConverterFactory and assign it to the JSON column.

public static readonly JsonSerializerOptions EntityJsonOptions = CreateOptions();
static JsonSerializerOptions CreateOptions()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new ScalarJsonConverterFactory());
return options;
}

In your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Customer>()
.Property(c => c.Email)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, EntityJsonOptions),
v => JsonSerializer.Deserialize<EmailAddress>(v, EntityJsonOptions)!
);
}

Because scalar value objects serialize to a single primitive, the stored JSON is compact and query-friendly.

For scalar value objects you can also use a plain EF ValueConverter without JSON, mapping directly to the underlying primitive:

builder
.Entity<Customer>()
.Property(c => c.Email)
.HasConversion(
v => v.Value,
v => EmailAddress.Hydrate(v)
);

Hydrate is used so that already-validated persisted values are not re-validated on read.

Complex [ValueObject] types serialize as an object graph. Store them in a JSON column with the same pattern, using the generated [JsonConverter] (present by default) or the shared options.

  • Scalar value objects with primitive inner values map naturally to the underlying primitive for filtering.
  • For complex values stored as JSON, deep predicates translate depending on the provider and column type. Test the exact predicate against your provider before relying on it.
  • [ValueObject] types generate a private parameterless constructor by default (see [ValueObjectDefaults]) to support EF Core materialization.
  • Value objects are immutable; EF tracks them by value like any struct/record.

See src/src/Sample for a runnable DTO + JSON-column example.