Skip to content

Fix #4575: project mt_created_at onto MapTo'd member in v9#4577

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/4575-created-at-mapping
May 28, 2026
Merged

Fix #4575: project mt_created_at onto MapTo'd member in v9#4577
jeremydmiller merged 1 commit into
masterfrom
fix/4575-created-at-mapping

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #4575.

Bug

In Marten 9.x, m.CreatedAt.MapTo(d => d.CreatedOn) configures the column but the mapped property stays at default(DateTimeOffset) after a load:

options.Schema.For<WebToSmsProgress>()
    .Metadata(m =>
    {
        m.LastModified.MapTo(f => f.LastModifiedOn);   // works
        m.CreatedAt.MapTo(f => f.CreatedOn);            // broken in v9
        m.Revision.MapTo(f => f.Version);               // works
    });

Worked in v8, regressed in v9.

Cause

The closed-shape storage rewrite (#4498 — "Scrub all remaining JasperFx.CodeGeneration usages from Marten") ported every metadata column from the codegen path into DocumentStorageDescriptorBuilder except CreatedAt. The column still gets Enabled = true and is selected from the DB (CreatedAtColumn.ShouldSelect returns Member != null), but no binder copies the value from the reader to the mapped member after deserialization.

Confirmed via diff inspection: DocumentStorageDescriptorBuilder wires TenantId, LastModified, CorrelationId, CausationId, LastModifiedBy, Headers, duplicated fields, and soft-delete; CreatedAt is the only ISelectableColumn metadata column with no binder.

Fix

Add DocumentCreatedAtBinder<TDoc> (mirroring DocumentLastModifiedBinder / DocumentTenantIdBinder) and add it to readBinders right after the LastModified slot — mt_created_at sits between mt_last_modified and mt_dotnet_type in the table (DocumentTable.cs:51); mt_dotnet_type is not ISelectableColumn, so in the SELECT projection the column lands immediately after mt_last_modified.

The binder is read-only, deliberately not added to writeBinders:

  • CreatedAtColumn carries a transaction_timestamp() DEFAULT, so PostgreSQL fills the value on insert.
  • CreatedAt is immutable thereafter; participating in writeBinders would put the column into the UPDATE SET list and clobber the original creation time on every subsequent save.

Test

when_mapping_to_the_created_at_metadata added to flexible_document_metadata.cs — uses the existing FlexibleDocumentMetadataContext fixture. Two new cases:

  • created_at_is_populated_on_query_only — the primary regression: confirms the mapped member is non-default after reload.
  • created_at_is_immutable_across_updates — pins the read-only invariant so a future change adding CreatedAt to writeBinders would fail loudly.

Both pass locally (10/10 in the new fixture, including the 8 inherited from the base context).

🤖 Generated with Claude Code

The closed-shape rewrite (#4498) ported every other metadata column from
the JasperFx.CodeGeneration path into ClosedShape but missed wiring the
mt_created_at read-back. Result: in v9.x, `m.CreatedAt.MapTo(d => d.X)`
enables the column and selects it from the DB, but no binder copies the
value onto the mapped member, so `X` stays at `default(DateTimeOffset)`
after a load. Sibling configurations (`LastModified.MapTo`,
`Revision.MapTo`) still work because their binders were ported.

This adds the missing `DocumentCreatedAtBinder<TDoc>` and wires it into
`DocumentStorageDescriptorBuilder` after the LastModified slot —
mt_created_at sits between mt_last_modified and mt_dotnet_type in the
table (DocumentTable line 51) and mt_dotnet_type is NOT
ISelectableColumn, so in the SELECT projection the column lands
immediately after mt_last_modified. The binder is **read-only** —
`CreatedAtColumn` carries a `transaction_timestamp()` DEFAULT for
insert, and the value is immutable after that, so adding it to
writeBinders would put it into the UPDATE SET list and clobber the
original creation time on every subsequent save.

Regression test in `flexible_document_metadata.cs`: both the populate-
on-load path and the immutable-across-update invariant are covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CreatedAt metadata property mapping issue

1 participant