diff --git a/docs/events/storage.md b/docs/events/storage.md index 25f8e3ba9f..33291eedd8 100644 --- a/docs/events/storage.md +++ b/docs/events/storage.md @@ -108,6 +108,39 @@ public Dictionary? Headers { get; set; } The full event data is available on `EventStream` and `IEvent` objects immediately after committing a transaction that involves event capture. See [diagnostics and instrumentation](/diagnostics) for more information on capturing event data in the instrumentation hooks. +## Event Type Names + +If you look into the `mt_events` table in your system you'll see a column named `type` that will +have an alias for the .NET type name that Marten keys off when reading events from the database +to "know" what .NET type to deserialize the JSON data to. + +The original idea was that people should be able to easily move event types around in their +solution without breaking the storage as full type names changed, so we purposely used _only_ the +type name of the .NET type for the event alias. In real life usage though, sometimes people will +use completely different .NET types with the same type name like in this example: + +```csharp +public class GroupEvents +{ + public record Created(string Name); +} + +public class UserEvents +{ + public record Created(string Name); +} +``` + +In that case, the original naming scheme of "created" will not correctly disambiguate between the +two different `Created` types above. While you _could_ manually alias all of these event types +yourself to disambiguate, it's too easy to forget to do that. Instead, you can just switch to different +naming schemes like this: + +snippet: sample_event_naming_style + +Note that you will have to switch out of the "classic" naming mode to disambiguate between event types +with the same class name in different namespaces. + ## Optional Indexes As of Marten 7.0, Marten is omitting indexes that aren't universally necessary, but diff --git a/src/EventSourcingTests/Bugs/Bug_3880_able_to_use_different_event_types_with_same_name.cs b/src/EventSourcingTests/Bugs/Bug_3880_able_to_use_different_event_types_with_same_name.cs new file mode 100644 index 0000000000..7871ccb1af --- /dev/null +++ b/src/EventSourcingTests/Bugs/Bug_3880_able_to_use_different_event_types_with_same_name.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using JasperFx.Events; +using Marten.Testing.Harness; +using Shouldly; +using Xunit; + +namespace EventSourcingTests.Bugs; + +public class Bug_3880_able_to_use_different_event_types_with_same_name : BugIntegrationContext +{ + [Theory] + [InlineData(EventNamingStyle.SmarterTypeName)] + [InlineData(EventNamingStyle.FullTypeName)] + public async Task append_and_load(EventNamingStyle namingStyle) + { + StoreOptions(opts => + { + opts.Events.EventNamingStyle = namingStyle; + }); + + var streamId = Guid.NewGuid(); + theSession.Events.StartStream(streamId, new GroupEvents.Created("foo"), new UserEvents.Created("bar")); + await theSession.SaveChangesAsync(); + + var events = await theSession.Events.FetchStreamAsync(streamId); + + events[0].ShouldBeOfType>().Data.Name.ShouldBe("foo"); + events[1].ShouldBeOfType>().Data.Name.ShouldBe("bar"); + } +} + +public class GroupEvents +{ + public record Created(string Name); +} + +public class UserEvents +{ + public record Created(string Name); +} diff --git a/src/EventSourcingTests/EventGraphTests.cs b/src/EventSourcingTests/EventGraphTests.cs index adcbdc2340..960e2fe8c2 100644 --- a/src/EventSourcingTests/EventGraphTests.cs +++ b/src/EventSourcingTests/EventGraphTests.cs @@ -22,6 +22,33 @@ public EventGraphTests() theGraph = new StoreOptions().EventGraph; } + [Fact] + public void default_naming_style_is_classic() + { + theGraph.EventNamingStyle.ShouldBe(EventNamingStyle.ClassicTypeName); + } + + [Fact] + public void override_the_naming_style_1() + { + theGraph.EventNamingStyle = EventNamingStyle.ClassicTypeName; + theGraph.EventMappingFor().EventTypeName.ShouldBe("a_event"); + } + + [Fact] + public void override_the_naming_style_2() + { + theGraph.EventNamingStyle = EventNamingStyle.SmarterTypeName; + theGraph.EventMappingFor().EventTypeName.ShouldBe("user_events.created"); + } + + [Fact] + public void override_the_naming_style_3() + { + theGraph.EventNamingStyle = EventNamingStyle.FullTypeName; + theGraph.EventMappingFor().EventTypeName.ShouldBe("EventSourcingTests.UserEvents.Created"); + } + [Fact] public void get_backwards_compatible_name_for_archived() { @@ -183,3 +210,13 @@ public void Apply(IssueAssigned e) // Do stuff } } + +public class GroupEvents +{ + public record Created(string Name); +} + +public class UserEvents +{ + public record Created(string Name); +} diff --git a/src/EventSourcingTests/Examples/NamingStyles.cs b/src/EventSourcingTests/Examples/NamingStyles.cs new file mode 100644 index 0000000000..bb7d8b92d0 --- /dev/null +++ b/src/EventSourcingTests/Examples/NamingStyles.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using JasperFx.Events; +using Marten; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace EventSourcingTests.Examples; + +public class NamingStyles +{ + public static void configure_different_naming_styles() + { + #region sample_event_naming_style + + var builder = Host.CreateApplicationBuilder(); + builder.Services.AddMarten(opts => + { + opts.Connection(builder.Configuration.GetConnectionString("marten")); + + + // This is the default behavior, but just showing you that + // this is an option + opts.Events.EventNamingStyle = EventNamingStyle.ClassicTypeName; + + // This mode is "the classic style Marten has always used, except smart enough + // to disambiguate inner classes that have the same type name" + opts.Events.EventNamingStyle = EventNamingStyle.SmarterTypeName; + + // Forget all the pretty naming aliases, just use the .NET full type name for + // the event type name + opts.Events.EventNamingStyle = EventNamingStyle.FullTypeName; + }); + + #endregion + } +} diff --git a/src/Marten/Events/EventGraph.cs b/src/Marten/Events/EventGraph.cs index ea17e56ec8..dcb7b01b80 100644 --- a/src/Marten/Events/EventGraph.cs +++ b/src/Marten/Events/EventGraph.cs @@ -78,6 +78,11 @@ internal EventGraph(StoreOptions options) } + /// + /// Opt into different aliasing styles for .NET event types + /// + public EventNamingStyle EventNamingStyle { get; set; } = EventNamingStyle.ClassicTypeName; + internal NpgsqlDbType StreamIdDbType { get; private set; } internal StoreOptions Options { get; } diff --git a/src/Marten/Events/EventMapping.cs b/src/Marten/Events/EventMapping.cs index d6b7461bd4..3de068aa9f 100644 --- a/src/Marten/Events/EventMapping.cs +++ b/src/Marten/Events/EventMapping.cs @@ -42,6 +42,8 @@ protected EventMapping(EventGraph parent, Type eventType) : base(eventType) { TenancyStyle = parent.TenancyStyle; + EventTypeName = eventType.GetEventTypeName(parent.EventNamingStyle); + _parent = parent; DocumentType = eventType; diff --git a/src/Marten/Events/IEventStoreOptions.cs b/src/Marten/Events/IEventStoreOptions.cs index 277671cdf7..f3c6f6d29b 100644 --- a/src/Marten/Events/IEventStoreOptions.cs +++ b/src/Marten/Events/IEventStoreOptions.cs @@ -91,6 +91,11 @@ public interface IEventStoreOptions /// public bool UseMandatoryStreamTypeDeclaration { get; set; } + /// + /// Opt into different aliasing styles for .NET event types + /// + public EventNamingStyle EventNamingStyle { get; set; } + /// /// Register an event type with Marten. This isn't strictly necessary for normal usage, /// but can help Marten with asynchronous projections where Marten hasn't yet encountered diff --git a/src/Marten/Events/IReadOnlyEventStoreOptions.cs b/src/Marten/Events/IReadOnlyEventStoreOptions.cs index 81890cfced..26ec148ea7 100644 --- a/src/Marten/Events/IReadOnlyEventStoreOptions.cs +++ b/src/Marten/Events/IReadOnlyEventStoreOptions.cs @@ -86,4 +86,9 @@ public interface IReadOnlyEventStoreOptions /// but this will be true in 8.0 /// bool UseMandatoryStreamTypeDeclaration { get; set; } + + /// + /// Opt into different aliasing styles for .NET event types + /// + EventNamingStyle EventNamingStyle { get; set; } } diff --git a/src/Marten/Marten.csproj b/src/Marten/Marten.csproj index b6cd9f604f..a870814745 100644 --- a/src/Marten/Marten.csproj +++ b/src/Marten/Marten.csproj @@ -34,7 +34,7 @@ - +