Skip to content

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 .Value property returning Guid)

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

  • WHIZ031 - Event type has multiple [StreamId] attributes
  • WHIZ009 - Warning for IEvent/ICommand implementations missing [StreamId] (general case)

See Also