WHIZ030: Perspective Event Missing StreamId¶
Verified by tests
PerspectiveDiscoveryGeneratorTests — library CI run #31657041675 (2026-08-13)
Severity: Error Category: Whizbang.SourceGeneration
Description¶
This error occurs when an event type is used in a perspective (via IPerspectiveFor<TModel, TEvent>) but does not have a property marked with the [StreamId] attribute.
The [StreamId] attribute is required on all events used in perspectives because it identifies which stream (aggregate) the event belongs to. This enables the perspective runner to:
- Group events by stream for ordered processing
- Apply events in UUID7 timestamp order within each stream
- Maintain consistency per aggregate
The check searches the event type's full inheritance hierarchy, so a [StreamId] inherited from a base event type satisfies it. For array event types (batch application), the element type is validated.
Error Message¶
Event type 'ProductCreatedEvent' used in perspective 'ProductCatalogPerspective' must have exactly one property marked with [StreamId] attribute
How to Fix¶
Add the [StreamId] attribute to exactly one property on your event type:
How to Fix
public record ProductCreatedEvent : IEvent {
[StreamId] // ✅ Add this attribute
public Guid ProductId { get; init; }
public string Name { get; init; } = string.Empty;
public decimal Price { get; init; }
}
Requirements¶
- Exactly one property must have
[StreamId](more than one triggers WHIZ031) - The property should identify the aggregate/stream (e.g.,
OrderId,ProductId,CustomerId) - The property must be of type
Guid,Guid?, or a WhizbangId type (a type with a.Valueproperty returningGuid)
Example: Product Catalog Perspective¶
Before (causes WHIZ030): Example: Product Catalog Perspective
// ❌ Missing [StreamId] attribute
public record ProductCreatedEvent : IEvent {
public Guid ProductId { get; init; } // No [StreamId]!
public string Name { get; init; } = string.Empty;
}
public record ProductDto {
[StreamId]
public Guid ProductId { get; init; }
public string Name { get; init; } = string.Empty;
}
public class ProductCatalogPerspective : IPerspectiveFor<ProductDto, ProductCreatedEvent> {
public ProductDto Apply(ProductDto currentData, ProductCreatedEvent @event) {
return new ProductDto {
ProductId = @event.ProductId,
Name = @event.Name
};
}
}
After (error resolved): Example: Product Catalog Perspective - ProductCreatedEvent
// ✅ [StreamId] added to event
public record ProductCreatedEvent : IEvent {
[StreamId] // Identifies the stream
public Guid ProductId { get; init; }
public string Name { get; init; } = string.Empty;
}
public record ProductDto {
[StreamId]
public Guid ProductId { get; init; }
public string Name { get; init; } = string.Empty;
}
public class ProductCatalogPerspective : IPerspectiveFor<ProductDto, ProductCreatedEvent> {
public ProductDto Apply(ProductDto currentData, ProductCreatedEvent @event) {
return new ProductDto {
ProductId = @event.ProductId,
Name = @event.Name
};
}
}
Why This Matters¶
Without a [StreamId], the perspective runner cannot:
1. Extract the stream ID from events at compile-time (zero reflection)
2. Group events by stream for ordered processing
3. Apply events in order within each aggregate
4. Maintain consistency per aggregate instance
Related Diagnostics¶
- WHIZ031 - Event type has multiple
[StreamId]attributes - WHIZ009 - Warning for IEvent/ICommand implementations missing
[StreamId](general case)
See Also¶
- StreamId Attribute - Detailed attribute documentation
- Perspectives - Pure function perspectives with StreamId
- PerspectiveRunner Architecture - How runners use StreamId