diff --git a/src/EventSourcingTests/Bugs/Bug_4178_nested_versioned_events_dispatch_to_wrong_projection_apply_overload.cs b/src/EventSourcingTests/Bugs/Bug_4178_nested_versioned_events_dispatch_to_wrong_projection_apply_overload.cs new file mode 100644 index 0000000000..2f66e45bc5 --- /dev/null +++ b/src/EventSourcingTests/Bugs/Bug_4178_nested_versioned_events_dispatch_to_wrong_projection_apply_overload.cs @@ -0,0 +1,134 @@ +using System; +using System.Threading.Tasks; +using JasperFx.Events; +using Marten.Testing.Harness; +using Shouldly; +using Xunit; + +namespace EventSourcingTests.Bugs +{ + public class Bug_4178_nested_versioned_events_dispatch_to_wrong_projection_apply_overload: BugIntegrationContext + { + [Fact] + public async Task classic_naming_dispatches_v1_and_v2_to_separate_apply_overloads() + { + StoreOptions(opts => + { + opts.Events.EventNamingStyle = EventNamingStyle.ClassicTypeName; + }); + + var streamId = Guid.NewGuid(); + + theSession.Events.StartStream(streamId, + new VersionedCustomerEvents.V1.CustomerCreated(streamId, "Alice"), + new VersionedCustomerEvents.V2.CustomerCreated(streamId, "Alice Updated", "alice@example.com")); + + await theSession.SaveChangesAsync(); + + var customer = await theSession.Events.AggregateStreamAsync(streamId); + + customer.ShouldNotBeNull(); + + customer.V1ApplyCallCount.ShouldBe(1, "V1 Apply must be called exactly once"); + customer.V2ApplyCallCount.ShouldBe(1, "V2 Apply must be called exactly once"); + customer.Name.ShouldBe("Alice Updated"); + customer.Email.ShouldBe("alice@example.com"); + } + + [Fact] + public async Task classic_naming_raw_event_types_round_trip_correctly() + { + StoreOptions(opts => + { + opts.Events.EventNamingStyle = EventNamingStyle.ClassicTypeName; + }); + + var streamId = Guid.NewGuid(); + + theSession.Events.StartStream(streamId, + new VersionedCustomerEvents.V1.CustomerCreated(streamId, "Alice"), + new VersionedCustomerEvents.V2.CustomerCreated(streamId, "Bob", "bob@example.com")); + await theSession.SaveChangesAsync(); + + var events = await theSession.Events.FetchStreamAsync(streamId); + + events.Count.ShouldBe(2); + + events[0].ShouldBeOfType>( + "first event must deserialise as V1.CustomerCreated"); + events[1].ShouldBeOfType>( + "second event must deserialise as V2.CustomerCreated"); + + var v1Data = ((Event)events[0]).Data; + v1Data.Name.ShouldBe("Alice"); + + var v2Data = ((Event)events[1]).Data; + v2Data.Name.ShouldBe("Bob"); + v2Data.Email.ShouldBe("bob@example.com"); + } + + [Theory] + [InlineData(EventNamingStyle.SmarterTypeName)] + [InlineData(EventNamingStyle.FullTypeName)] + public async Task smarter_and_full_naming_also_dispatch_correctly(EventNamingStyle namingStyle) + { + StoreOptions(opts => + { + opts.Events.EventNamingStyle = namingStyle; + }); + + var streamId = Guid.NewGuid(); + + theSession.Events.StartStream(streamId, + new VersionedCustomerEvents.V1.CustomerCreated(streamId, "Alice"), + new VersionedCustomerEvents.V2.CustomerCreated(streamId, "Alice Updated", "alice@example.com")); + await theSession.SaveChangesAsync(); + + var customer = await theSession.Events.AggregateStreamAsync(streamId); + + customer.ShouldNotBeNull(); + customer.V1ApplyCallCount.ShouldBe(1); + customer.V2ApplyCallCount.ShouldBe(1); + customer.Email.ShouldBe("alice@example.com"); + } + } + + public static class VersionedCustomerEvents + { + public static class V1 + { + public record CustomerCreated(Guid Id, string Name); + } + + public static class V2 + { + public record CustomerCreated(Guid Id, string Name, string Email); + } + } + + public class VersionedCustomer + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + + public int V1ApplyCallCount { get; set; } + + public int V2ApplyCallCount { get; set; } + + public void Apply(IEvent e) + { + Id = e.Data.Id; + Name = e.Data.Name; + V1ApplyCallCount++; + } + + public void Apply(IEvent e) + { + Id = e.Data.Id; + Name = e.Data.Name; + Email = e.Data.Email; + V2ApplyCallCount++; + } + } +} diff --git a/src/Marten/Events/EventDocumentStorage.cs b/src/Marten/Events/EventDocumentStorage.cs index 5090998f1d..04ac9707dc 100644 --- a/src/Marten/Events/EventDocumentStorage.cs +++ b/src/Marten/Events/EventDocumentStorage.cs @@ -246,6 +246,18 @@ public IEvent Resolve(DbDataReader reader) mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName); } + else if (!reader.IsDBNull(2)) + { + var dotnetTypeName = reader.GetFieldValue(2); + if (!string.IsNullOrEmpty(dotnetTypeName) && dotnetTypeName != mapping.DotNetTypeName) + { + var altMapping = Events.TryGetRegisteredMappingForDotNetTypeName(dotnetTypeName); + if (altMapping != null) + { + mapping = altMapping; + } + } + } var @event = mapping.ReadEventData(_serializer, reader); @@ -264,6 +276,18 @@ public async Task ResolveAsync(DbDataReader reader, CancellationToken to mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName); } + else if (!await reader.IsDBNullAsync(2, token).ConfigureAwait(false)) + { + var dotnetTypeName = await reader.GetFieldValueAsync(2, token).ConfigureAwait(false); + if (!string.IsNullOrEmpty(dotnetTypeName) && dotnetTypeName != mapping.DotNetTypeName) + { + var altMapping = Events.TryGetRegisteredMappingForDotNetTypeName(dotnetTypeName); + if (altMapping != null) + { + mapping = altMapping; + } + } + } IEvent @event; try diff --git a/src/Marten/Events/EventGraph.cs b/src/Marten/Events/EventGraph.cs index 1e5a5c3245..7db884ae28 100644 --- a/src/Marten/Events/EventGraph.cs +++ b/src/Marten/Events/EventGraph.cs @@ -478,6 +478,11 @@ internal IEnumerable AllEvents() return _byEventName[eventType]; } + internal EventMapping? TryGetRegisteredMappingForDotNetTypeName(string dotnetTypeName) + { + return AllEvents().FirstOrDefault(x => x.DotNetTypeName == dotnetTypeName); + } + // Fetch additional event aliases that map to these types internal IReadOnlySet AliasesForEvents(IReadOnlyCollection types) {