GenerateStreamId Attribute¶
Verified by tests
GenerateStreamIdGeneratorTests, StreamIdGeneratorTests, StreamIdGuardTests — library CI run #31657041675 (2026-08-13)
The [GenerateStreamId] attribute marks an event for automatic StreamId generation at dispatch time. It works alongside [StreamId] to provide explicit, per-event-type opt-in for stream ID auto-generation.
Namespace¶
Namespace
Syntax¶
Syntax
// On a property (alongside [StreamId])
public record OrderCreatedEvent : IEvent {
[StreamId] [GenerateStreamId]
public Guid OrderId { get; set; }
}
// With OnlyIfEmpty for flexible events
public record InventoryReserved : IEvent {
[StreamId] [GenerateStreamId(OnlyIfEmpty = true)]
public Guid ReservationId { get; set; }
}
// On a class (for inherited [StreamId])
[GenerateStreamId]
public record OrderCreatedEvent : BaseEvent {
// [StreamId] inherited from BaseEvent.StreamId
}
// On record parameters
public record OrderCreated([property: StreamId] [property: GenerateStreamId] Guid OrderId) : IEvent;
Applies To¶
- Properties on event types (alongside
[StreamId]) - Record parameters (using
[property: GenerateStreamId]target) - Classes/records (when
[StreamId]is inherited from a base type)
Purpose¶
The [GenerateStreamId] attribute replaces the blunt AutoGenerateStreamIds option with fine-grained, per-event-type control over when StreamIds are auto-generated. (The legacy WhizbangOptions.AutoGenerateStreamIds property still exists for configuration compatibility but is no longer consulted by the pipeline.) This distinction is critical for event-sourced systems where:
- Stream-initiating events should always get a new StreamId
- Appending events must receive a StreamId from their parent (and fail-fast if missing)
- Flexible events may either inherit a StreamId from a cascade or generate their own
Event Patterns¶
Stream-Initiating Events¶
Events that start a new stream should always generate a new StreamId, even when cascaded from another event:
Stream-Initiating Events
public record OrderCreatedEvent : IEvent {
[StreamId] [GenerateStreamId]
public Guid OrderId { get; set; }
}
When dispatched, OrderId will always be populated with a new UUIDv7 (TrackedGuid.NewMedo()).
Appending Events¶
Events that must belong to an existing stream should NOT have [GenerateStreamId]. If the StreamId is Guid.Empty at dispatch time, the StreamIdGuard will throw an InvalidStreamIdException:
Appending Events
public record OrderItemAddedEvent : IEvent {
[StreamId]
public Guid OrderId { get; set; } // MUST be provided by caller
}
Flexible Events¶
Events that may be dispatched independently OR cascaded from a parent event should use OnlyIfEmpty = true:
Flexible Events
public record InventoryReserved : IEvent {
[StreamId] [GenerateStreamId(OnlyIfEmpty = true)]
public Guid ReservationId { get; set; }
}
- When cascaded: inherits parent's StreamId (not overwritten)
- When standalone: gets a new StreamId auto-generated
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
OnlyIfEmpty |
bool |
false |
When true, only generates a StreamId if the current value is Guid.Empty. When false, always generates a new StreamId. |
How It Works¶
-
At compile time: The source generator discovers
[GenerateStreamId]attributes and generates aGetGenerationPolicymethod with type-specific switch arms. -
At dispatch time: The Dispatcher calls
GetGenerationPolicy(message)to determine if auto-generation should occur: (ShouldGenerate: true, OnlyIfEmpty: false)→ Always generate new StreamId(ShouldGenerate: true, OnlyIfEmpty: true)→ Generate only if current StreamId isGuid.Empty-
(ShouldGenerate: false, OnlyIfEmpty: false)→ No auto-generation; guard will throw if StreamId isGuid.Empty -
After generation:
StreamIdGuard.ThrowIfEmptyvalidates that events with[StreamId]have a non-empty StreamId at pipeline boundaries (Dispatcher outbox, consumer inbox, work coordinator queues).
Validation Guards¶
Events with [StreamId] but without [GenerateStreamId] are validated at pipeline boundaries:
- Dispatcher outbox:
StreamIdGuard.ThrowIfEmptythrowsInvalidStreamIdException - Consumer inbox: Guards at
TransportConsumerWorkerandServiceBusConsumerWorker - Work coordinator:
StreamIdGuard.ThrowIfNonNullEmptyat queue boundaries
The guard distinguishes between:
- null StreamId → OK (event has no stream concept, no [StreamId] attribute)
- Guid.Empty StreamId → Bug (event has [StreamId] but no value was provided or generated)
- Valid Guid → OK
Relationship to Other Attributes¶
| Attribute | Purpose |
|---|---|
[StreamId] |
Marks which property IS the stream ID |
[GenerateStreamId] |
Controls whether the stream ID is AUTO-GENERATED |
[GenerateStreamId] requires [StreamId] to be present (either on the same property or inherited from a base class).
Zero Reflection / AOT¶
The [GenerateStreamId] attribute is fully AOT-compatible:
- Discovery happens at compile time via the source generator
- The
GetGenerationPolicymethod uses type-based pattern matching (no reflection) - Generation uses
TrackedGuid.NewMedo()(UUIDv7) for time-ordered, database-friendly IDs
See Also¶
- StreamId Attribute — Marks the stream ID property
- Stream ID Concepts — Stream ID concepts and ordering