Skip to content

StreamKey Attribute

Verified by tests

StreamIdGeneratorTests, StreamIdExtractorTests — library CI run #31657041675 (2026-08-13)

Updated

The [StreamKey] attribute no longer exists as a separate attribute. It has been unified into [StreamId], which now serves both purposes: identifying the event store stream an event belongs to, and grouping/ordering events per stream for perspective processing. Apply [StreamId] everywhere older documentation said [StreamKey].

Migration

StreamKey to StreamId

// Before (historical API - no longer compiles)
public record ProductCreatedEvent : IEvent {
  [StreamKey]
  public Guid ProductId { get; init; }
}

// After (current API)
public record ProductCreatedEvent : IEvent {
  [StreamId]
  public Guid ProductId { get; init; }
}

The same replacement applies to perspective model types - models mark their stream identity property with [StreamId]:

Model StreamId

public record ProductDto {
  [StreamId]  // Identifies which product this model represents
  public Guid ProductId { get; init; }
  public string Name { get; init; } = string.Empty;
  public decimal Price { get; init; }
}

public class ProductCatalogPerspective : IPerspectiveFor<ProductDto, ProductCreatedEvent> {
  public ProductDto Apply(ProductDto currentData, ProductCreatedEvent eventData) {
    return new ProductDto {
      ProductId = eventData.ProductId,
      Name = eventData.Name,
      Price = eventData.Price
    };
  }
}

How Perspective Ordering Works Today

The role the historical [StreamKey] attribute played in perspectives is now filled by [StreamId]:

  1. The source generators discover [StreamId] on event, command, and perspective model properties and generate zero-reflection extractors (StreamIdExtractors in your assembly's .Generated namespace).
  2. At runtime, perspective processing groups events by their extracted stream ID, so all events for one aggregate (e.g. Order #123) are applied in order within that stream.
  3. Events are applied to the model through pure Apply() methods; the updated model is saved with its checkpoint per stream.

Requirements

  • Exactly one [StreamId] property per event type used in a perspective
  • Perspective model types must also have a [StreamId] property, or no runner is generated
  • Property type must yield a Guid: Guid, Guid?, a WhizbangId-style value type, or a string/other type whose value parses as a Guid (null or non-parseable values extract no stream ID)

Diagnostics

The source generators validate [StreamId] usage for perspectives:

WHIZ030: Missing StreamId

Error: An event used in a perspective has no [StreamId] property.

WHIZ030: Missing StreamId

// ❌ Causes WHIZ030
public record ProductEvent : IEvent {
  public Guid ProductId { get; init; }  // No [StreamId]!
}

// ✅ Fixed
public record ProductEvent : IEvent {
  [StreamId]
  public Guid ProductId { get; init; }
}

See WHIZ030 Diagnostic for details.

WHIZ031: Multiple StreamIds

Error: An event has multiple properties marked with [StreamId].

WHIZ031: Multiple StreamIds

// ❌ Causes WHIZ031
public record OrderEvent : IEvent {
  [StreamId]
  public Guid OrderId { get; init; }

  [StreamId]  // Only one [StreamId] allowed!
  public Guid CustomerId { get; init; }
}

// ✅ Fixed - Choose the primary aggregate
public record OrderEvent : IEvent {
  [StreamId]  // Order is the primary aggregate
  public Guid OrderId { get; init; }

  public Guid CustomerId { get; init; }  // Related entity, not stream ID
}

See WHIZ031 Diagnostic for details.

WHIZ033: Perspective Model Missing StreamId

Warning: A perspective's model type has no [StreamId] property. The perspective will not get a generated runner until the model marks its stream identity property.

Best Practices

  • Put [StreamId] on the aggregate root identifier (e.g. OrderId), not on related entity IDs or timestamps
  • Use the same property name across all events of one aggregate and on the matching model
  • Prefer time-ordered UUIDv7 values (TrackedGuid.NewMedo() or GenerateStreamId) for stream IDs

See Also