Skip to content

Unwrap Nullable<T> when determining a Polecat aggregate's id type (GH-3942) - #3948

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/3942-nullable-saga-id-type
Aug 15, 2026
Merged

Unwrap Nullable<T> when determining a Polecat aggregate's id type (GH-3942)#3948
jeremydmiller merged 1 commit into
mainfrom
fix/3942-nullable-saga-id-type

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3942.

WriteModelAttribute.Modify asks the persistence provider for the aggregate's id type, and FindIdentity then branches on whether that answer is primitive. The two stores disagreed for a nullable id property:

store answer for an aggregate whose Id is AlertId? (a strong-typed record struct)
Marten the configured document id typestring there (StreamIdentity.AsString)
Polecat sagaType.GetProperty("Id").PropertyType verbatimNullable<AlertId>

Nullable<AlertId> is not primitive, so IsPrimitiveIdType was false, the documented IdentifiedBy<T> escape hatch was skipped entirely, and the message was scanned for a Nullable<AlertId> member — which exists in no codebase. The result was InvalidOperationException: Unable to determine an aggregate id, naming neither the type nor nullability.

That broke every [WriteModel] chain over such an aggregate in the Polecat/SQL Server flavour, while the identical shared source compiled and ran fine under Marten.

Change

Both reflection sites now unwrap, exactly as the issue suggested:

var idProp = sagaType.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance);
if (idProp == null) return typeof(Guid);
return Nullable.GetUnderlyingType(idProp.PropertyType) ?? idProp.PropertyType;
  • PolecatPersistenceFrameProvider.DetermineSagaIdType
  • DocumentExistsAttribute.ResolveIdType, whose doc comment already declared it matches the former — so they stay in lockstep

Nothing else changes: a non-nullable id, a string id, and a missing Id property all resolve exactly as before.

Verification

New Bugs/Bug_3942_nullable_aggregate_id_type.cs7 tests, 3 of which fail without this change (the two nullable cases on the saga provider, plus the document-resolution lockstep test). The rest pin that the untouched cases stay untouched.

These are deliberately reflection-level rather than integration tests. The failure is at codegen time, and under TypeLoadMode.Dynamic a chain only fails when its message first arrives — so an integration test would have to dispatch every message type to see it, which is why it took a codegen sweep across both flavours to surface at all.

Note string? erases to string at runtime, which is precisely why this only ever bit value-type ids; there is a test pinning that too.

🤖 Generated with Claude Code

https://claude.ai/code/session_014PwvC24c5dNxv7DJR7RUjv

…-3942)

WriteModelAttribute.Modify asks the persistence provider for the aggregate's
id type, and FindIdentity branches on whether that answer is primitive. The
two stores disagreed for a nullable id property:

  Marten   the configured document id type -- never nullable
  Polecat  sagaType.GetProperty("Id").PropertyType verbatim

So `public AlertId? Id { get; set; }` answered Nullable<AlertId> under
Polecat. That is not primitive, so IsPrimitiveIdType was false, the
documented IdentifiedBy<T> escape hatch was skipped entirely, and the
message was scanned for a Nullable<AlertId> member -- which exists in no
codebase. Every [WriteModel] chain over such an aggregate failed to build
under the Polecat flavour while the identical shared source compiled and ran
under Marten.

Both reflection sites now unwrap: DetermineSagaIdType and the
DocumentExistsAttribute.ResolveIdType that is documented to match it.
Nothing else changes -- a non-nullable id, a string id, and a missing Id
property all resolve exactly as before.

The failure needs both a nullable id property and the Polecat flavour, so it
is invisible to a Marten-only run, and it is a codegen-time failure that
under TypeLoadMode.Dynamic only surfaces when the chain's message first
arrives. The new tests are therefore reflection-level rather than
integration: 7 tests, 3 of which fail without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014PwvC24c5dNxv7DJR7RUjv
@jeremydmiller
jeremydmiller merged commit 76b9931 into main Aug 15, 2026
71 of 72 checks passed
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.

Wolverine.Polecat: DetermineSagaIdType should unwrap Nullable<T>, so the two stores agree on an aggregate's id type

1 participant