Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<VersionedCustomer>(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<VersionedCustomer>(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<Event<VersionedCustomerEvents.V1.CustomerCreated>>(
"first event must deserialise as V1.CustomerCreated");
events[1].ShouldBeOfType<Event<VersionedCustomerEvents.V2.CustomerCreated>>(
"second event must deserialise as V2.CustomerCreated");

var v1Data = ((Event<VersionedCustomerEvents.V1.CustomerCreated>)events[0]).Data;
v1Data.Name.ShouldBe("Alice");

var v2Data = ((Event<VersionedCustomerEvents.V2.CustomerCreated>)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<VersionedCustomer>(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<VersionedCustomer>(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<VersionedCustomerEvents.V1.CustomerCreated> e)
{
Id = e.Data.Id;
Name = e.Data.Name;
V1ApplyCallCount++;
}

public void Apply(IEvent<VersionedCustomerEvents.V2.CustomerCreated> e)
{
Id = e.Data.Id;
Name = e.Data.Name;
Email = e.Data.Email;
V2ApplyCallCount++;
}
}
}
24 changes: 24 additions & 0 deletions src/Marten/Events/EventDocumentStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ public IEvent Resolve(DbDataReader reader)

mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName);
}
else if (!reader.IsDBNull(2))
{
var dotnetTypeName = reader.GetFieldValue<string>(2);
if (!string.IsNullOrEmpty(dotnetTypeName) && dotnetTypeName != mapping.DotNetTypeName)
{
var altMapping = Events.TryGetRegisteredMappingForDotNetTypeName(dotnetTypeName);
if (altMapping != null)
{
mapping = altMapping;
}
}
}

var @event = mapping.ReadEventData(_serializer, reader);

Expand All @@ -264,6 +276,18 @@ public async Task<IEvent> ResolveAsync(DbDataReader reader, CancellationToken to

mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName);
}
else if (!await reader.IsDBNullAsync(2, token).ConfigureAwait(false))
{
var dotnetTypeName = await reader.GetFieldValueAsync<string>(2, token).ConfigureAwait(false);
if (!string.IsNullOrEmpty(dotnetTypeName) && dotnetTypeName != mapping.DotNetTypeName)
{
var altMapping = Events.TryGetRegisteredMappingForDotNetTypeName(dotnetTypeName);
if (altMapping != null)
{
mapping = altMapping;
}
}
}

IEvent @event;
try
Expand Down
5 changes: 5 additions & 0 deletions src/Marten/Events/EventGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,11 @@ internal IEnumerable<EventMapping> 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<string> AliasesForEvents(IReadOnlyCollection<Type> types)
{
Expand Down
Loading