diff --git a/docs/events/storage.md b/docs/events/storage.md index dc925ab..1f35060 100644 --- a/docs/events/storage.md +++ b/docs/events/storage.md @@ -54,6 +54,26 @@ Polecat converts .NET event type names to snake_case for storage: - `MembersJoined` → `members_joined` - `InvoiceLineItemAdded` → `invoice_line_item_added` +## Pre-registering Event Types + +Event types are registered with the store on the fly the first time they are appended, so registering +them up front is optional. It is worth doing when the *reading* side may meet an event type before the +writing side does -- most commonly the async daemon in a separate process, which has to map an +`event_type_name` back to a .NET type it has not yet seen appended: + +```cs +var store = DocumentStore.For(opts => +{ + opts.Connection(connectionString); + + opts.Events.AddEventType(); + opts.Events.AddEventType(typeof(MembersJoined)); + opts.Events.AddEventTypes([typeof(MembersDeparted), typeof(QuestEnded)]); +}); +``` + +This mirrors Marten's `StoreOptions.Events.AddEventType()` / `AddEventTypes(...)`. + ## Sequence IDs Events are assigned a global, monotonically increasing sequence ID via SQL Server's `IDENTITY(1,1)`. This provides a total ordering of all events across all streams, which is critical for the async daemon's event processing. diff --git a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs index b9973a4..3a85212 100644 --- a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs +++ b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs @@ -118,9 +118,7 @@ public PolecatComplianceRegistrar(StoreOptions options) _options = options; } - // Straight to the registry: unlike Marten, Polecat's public EventStoreOptions facade has no - // AddEventType, so the shared IEventRegistry member on StoreOptions.EventGraph is the seam. - public void AddEventType(Type eventType) => _options.EventGraph.AddEventType(eventType); + public void AddEventType(Type eventType) => _options.Events.AddEventType(eventType); public ITagTypeRegistration RegisterTagType(string tableSuffix) where TTag : notnull => _options.Events.RegisterTagType(tableSuffix); diff --git a/src/Polecat.Tests/StoreOptionsTests.cs b/src/Polecat.Tests/StoreOptionsTests.cs index 037a0da..d07919f 100644 --- a/src/Polecat.Tests/StoreOptionsTests.cs +++ b/src/Polecat.Tests/StoreOptionsTests.cs @@ -108,4 +108,51 @@ public void can_configure_event_store_options() options.Events.EnableCausationId.ShouldBeTrue(); options.Events.EnableHeaders.ShouldBeTrue(); } + + [Fact] + public void add_event_type_generic_registers_the_event_type() + { + var options = new StoreOptions(); + options.Events.AddEventType(); + + options.EventGraph.AllKnownEventTypes() + .ShouldContain(x => x.EventType == typeof(StoreOptionsEventTypeA)); + } + + [Fact] + public void add_event_type_by_type_registers_the_event_type() + { + var options = new StoreOptions(); + options.Events.AddEventType(typeof(StoreOptionsEventTypeB)); + + options.EventGraph.AllKnownEventTypes() + .ShouldContain(x => x.EventType == typeof(StoreOptionsEventTypeB)); + } + + [Fact] + public void add_event_types_registers_all_of_them() + { + var options = new StoreOptions(); + options.Events.AddEventTypes([typeof(StoreOptionsEventTypeA), typeof(StoreOptionsEventTypeB)]); + + var known = options.EventGraph.AllKnownEventTypes(); + known.ShouldContain(x => x.EventType == typeof(StoreOptionsEventTypeA)); + known.ShouldContain(x => x.EventType == typeof(StoreOptionsEventTypeB)); + } + + [Fact] + public void add_event_type_is_idempotent() + { + var options = new StoreOptions(); + options.Events.AddEventType(); + options.Events.AddEventType(); + + options.EventGraph.AllKnownEventTypes() + .Count(x => x.EventType == typeof(StoreOptionsEventTypeA)) + .ShouldBe(1); + } } + +public record StoreOptionsEventTypeA(string Name); + +public record StoreOptionsEventTypeB(int Count); diff --git a/src/Polecat/StoreOptions.cs b/src/Polecat/StoreOptions.cs index 61f7e13..d78d31c 100644 --- a/src/Polecat/StoreOptions.cs +++ b/src/Polecat/StoreOptions.cs @@ -460,6 +460,36 @@ bool IEventStoreInstrumentation.ExtendedProgressionEnabled public Polecat.Events.Aggregation.IMessageOutbox MessageOutbox { get; set; } = Polecat.Events.Aggregation.NulloMessageOutbox.Instance; + /// + /// Pre-register an event type with the event store. Mirrors Marten's + /// StoreOptions.Events.AddEventType<TEvent>(). Not strictly necessary — event types are + /// registered on the fly as they are appended — but pre-registration can help with asynchronous + /// projections where the daemon process hasn't yet encountered the event type, and lets the + /// event type name alias be resolved before the first append. + /// + public void AddEventType() where TEvent : notnull + { + EventGraph!.AddEventType(typeof(TEvent)); + } + + /// + /// Pre-register an event type with the event store. Mirrors Marten's + /// StoreOptions.Events.AddEventType(Type). + /// + public void AddEventType(Type eventType) + { + EventGraph!.AddEventType(eventType); + } + + /// + /// Pre-register several event types with the event store in one call. Mirrors Marten's + /// StoreOptions.Events.AddEventTypes(IEnumerable<Type>). + /// + public void AddEventTypes(IEnumerable eventTypes) + { + foreach (var eventType in eventTypes) EventGraph!.AddEventType(eventType); + } + /// /// Register a tag type for Dynamic Consistency Boundary (DCB) support. /// Creates a tag table with an auto-generated suffix.