diff --git a/src/Polecat.Tests/Events/Bug_4214_identity_map_strong_typed_ids.cs b/src/Polecat.Tests/Events/Bug_4214_identity_map_strong_typed_ids.cs
new file mode 100644
index 0000000..39a52f8
--- /dev/null
+++ b/src/Polecat.Tests/Events/Bug_4214_identity_map_strong_typed_ids.cs
@@ -0,0 +1,100 @@
+using JasperFx.Events.Projections;
+using Polecat.Projections;
+using Polecat.Tests.Harness;
+using Polecat.Tests.Projections;
+
+namespace Polecat.Tests.Events;
+
+///
+/// Regression test for https://github.com/JasperFx/marten/issues/4214
+/// FetchForWriting throws InvalidCastException when using UseIdentityMapForAggregates
+/// with strongly typed IDs.
+///
+[Collection("integration")]
+public class Bug_4214_identity_map_strong_typed_ids : IntegrationContext
+{
+ public Bug_4214_identity_map_strong_typed_ids(DefaultStoreFixture fixture) : base(fixture)
+ {
+ }
+
+ [Fact]
+ public async Task fetch_for_writing_with_identity_map_and_strong_typed_guid_id_inline()
+ {
+ await StoreOptions(opts =>
+ {
+ opts.DatabaseSchemaName = "bug4214_inline";
+ opts.Projections.Add(new SingleStreamProjection(), ProjectionLifecycle.Inline);
+ opts.Projections.UseIdentityMapForAggregates = true;
+ });
+
+ await using var session = theStore.LightweightSession();
+
+ var id = Guid.NewGuid();
+ session.Events.StartStream(id,
+ new PaymentCreated(DateTimeOffset.UtcNow),
+ new PaymentVerified(DateTimeOffset.UtcNow));
+
+ await session.SaveChangesAsync();
+
+ // This threw InvalidCastException before the fix
+ var stream = await session.Events.FetchForWriting(id);
+ stream.Aggregate.ShouldNotBeNull();
+ stream.Aggregate!.State.ShouldBe(PaymentState.Verified);
+ }
+
+ [Fact]
+ public async Task fetch_for_writing_with_identity_map_and_strong_typed_guid_id_live()
+ {
+ await StoreOptions(opts =>
+ {
+ opts.DatabaseSchemaName = "bug4214_live";
+ opts.Projections.Add(new SingleStreamProjection(), ProjectionLifecycle.Live);
+ opts.Projections.UseIdentityMapForAggregates = true;
+ });
+
+ await using var session = theStore.LightweightSession();
+
+ var id = Guid.NewGuid();
+ session.Events.StartStream(id,
+ new PaymentCreated(DateTimeOffset.UtcNow),
+ new PaymentVerified(DateTimeOffset.UtcNow));
+
+ await session.SaveChangesAsync();
+
+ var stream = await session.Events.FetchForWriting(id);
+ stream.Aggregate.ShouldNotBeNull();
+ stream.Aggregate!.State.ShouldBe(PaymentState.Verified);
+ }
+
+ [Fact]
+ public async Task fetch_for_writing_twice_with_identity_map_and_strong_typed_guid_id_inline()
+ {
+ await StoreOptions(opts =>
+ {
+ opts.DatabaseSchemaName = "bug4214_twice";
+ opts.Projections.Add(new SingleStreamProjection(), ProjectionLifecycle.Inline);
+ opts.Projections.UseIdentityMapForAggregates = true;
+ });
+
+ await using var session = theStore.LightweightSession();
+
+ var id = Guid.NewGuid();
+ session.Events.StartStream(id,
+ new PaymentCreated(DateTimeOffset.UtcNow),
+ new PaymentVerified(DateTimeOffset.UtcNow));
+
+ await session.SaveChangesAsync();
+
+ // First fetch stores in identity map
+ var stream1 = await session.Events.FetchForWriting(id);
+ stream1.Aggregate.ShouldNotBeNull();
+
+ stream1.AppendOne(new PaymentCanceled(DateTimeOffset.UtcNow));
+ await session.SaveChangesAsync();
+
+ // Second fetch should retrieve from identity map without cast error
+ var stream2 = await session.Events.FetchForWriting(id);
+ stream2.Aggregate.ShouldNotBeNull();
+ stream2.Aggregate!.State.ShouldBe(PaymentState.Canceled);
+ }
+}
diff --git a/src/Polecat/Internal/QuerySession.cs b/src/Polecat/Internal/QuerySession.cs
index 9202419..8c4f69b 100644
--- a/src/Polecat/Internal/QuerySession.cs
+++ b/src/Polecat/Internal/QuerySession.cs
@@ -36,7 +36,14 @@ internal void StoreAggregateInIdentityMap(TId id, TDoc document)
{
if (AggregateIdentityMap.TryGetValue(typeof(TDoc), out var raw))
{
- ((Dictionary)raw)[id] = document;
+ if (raw is Dictionary typedDict)
+ {
+ typedDict[id] = document;
+ }
+ // else: The identity map was created with a different key type (e.g., a strong-typed ID
+ // like PaymentId while TId is Guid). The document is already stored by the inline
+ // projection under the strong-typed key, so we skip storing it again to avoid
+ // replacing the dictionary with an incompatible key type.
}
else
{