Skip to content

WHIZ300: Inconsistent Perspective Model Types

Verified by tests

PerspectiveModelConsistencyAnalyzerTests — library CI run #31657041675 (2026-08-13)

Severity: Error Category: Perspective Validation

Description

This error is reported when a class implements multiple IPerspectiveFor<TModel, TEvent> and/or IPerspectiveWithActionsFor<TModel, TEvent> interfaces with different TModel types. All perspective interfaces on a single class must share the same model type for the perspective runner to function correctly.

The PerspectiveModelConsistencyAnalyzer catches this at compile time, preventing runtime failures in the perspective runner.

Diagnostic Message

Perspective '{ClassName}' implements multiple perspective interfaces with different model types: {ModelTypes}. All perspective interfaces on a class must use the same TModel type.

Common Causes

  1. Copy-paste error - Copying interface declarations from another perspective and forgetting to change the model type
  2. Refactoring mistake - Renaming or splitting a model type without updating all interface declarations
  3. Accidental mixing - Adding an interface for a different aggregate's model to the wrong perspective class

How to Fix

Ensure every IPerspectiveFor and IPerspectiveWithActionsFor interface on the class uses the same TModel type argument.

Before (causes WHIZ300)

Before (causes WHIZ300)

// WHIZ300: Different model types (OrderView vs ProductView)
public class OrderPerspective :
    IPerspectiveFor<OrderView, OrderCreated>,
    IPerspectiveWithActionsFor<ProductView, ProductDeleted> {

  public OrderView Apply(OrderView model, OrderCreated @event) {
    return model with { OrderId = @event.OrderId };
  }

  public ProductView Apply(ProductView model, ProductDeleted @event) {
    return model with { IsDeleted = true };
  }
}

Fix Option 1: Use the same model type

Fix Option 1: Use the same model type

public class OrderPerspective :
    IPerspectiveFor<OrderView, OrderCreated>,
    IPerspectiveWithActionsFor<OrderView, OrderDeleted> {

  public OrderView Apply(OrderView model, OrderCreated @event) {
    return model with { OrderId = @event.OrderId };
  }

  public OrderView Apply(OrderView model, OrderDeleted @event) {
    return model with { IsDeleted = true };
  }
}

Fix Option 2: Split into separate perspective classes

Fix Option 2: Split into separate perspectives

public class OrderPerspective :
    IPerspectiveFor<OrderView, OrderCreated> {

  public OrderView Apply(OrderView model, OrderCreated @event) {
    return model with { OrderId = @event.OrderId };
  }
}

public class ProductPerspective :
    IPerspectiveWithActionsFor<ProductView, ProductDeleted> {

  public ProductView Apply(ProductView model, ProductDeleted @event) {
    return model with { IsDeleted = true };
  }
}

Valid Usage Examples

Valid Usage Examples

// All interfaces use OrderView as TModel - no WHIZ300
public class OrderPerspective :
    IPerspectiveFor<OrderView, OrderCreated>,
    IPerspectiveFor<OrderView, OrderUpdated>,
    IPerspectiveWithActionsFor<OrderView, OrderDeleted>,
    IPerspectiveWithActionsFor<OrderView, OrderCancelled> {

  public OrderView Apply(OrderView model, OrderCreated @event) {
    return model with { OrderId = @event.OrderId, Status = "Created" };
  }

  public OrderView Apply(OrderView model, OrderUpdated @event) {
    return model with { Status = @event.NewStatus };
  }

  public OrderView Apply(OrderView model, OrderDeleted @event) {
    return model with { IsDeleted = true };
  }

  public OrderView Apply(OrderView model, OrderCancelled @event) {
    return model with { Status = "Cancelled" };
  }
}

Suppressing This Diagnostic

In rare cases where you intentionally want to suppress this error (e.g., testing analyzer behavior), use pragma suppression:

Suppressing This Diagnostic

#pragma warning disable WHIZ300 // Intentional inconsistent model types for testing
public class TestPerspective :
    IPerspectiveFor<OrderView, OrderCreated>,
    IPerspectiveWithActionsFor<ProductView, ProductDeleted> { }
#pragma warning restore WHIZ300

You can also suppress via .editorconfig:

[*.cs]
dotnet_diagnostic.WHIZ300.severity = none

Why This Matters

The perspective runner processes events by routing them to the correct Apply method on a perspective class. Internally, the generated runner and its storage registration resolve a single TModel per perspective class — one perspective table, one model type. When a class declares multiple perspective interfaces with different model types, the runner cannot determine which model to load and persist, and perspective processing fails at runtime.

The WHIZ300 analyzer moves this error to compile time, providing immediate feedback in your IDE.

Analyzer Details

Property Value
Diagnostic ID WHIZ300
Category Whizbang.PerspectiveValidation
Default Severity Error
Enabled by Default Yes
Analyzer PerspectiveModelConsistencyAnalyzer
ID Range WHIZ300-399 (perspective interface validation)

The analyzer registers on ClassDeclaration syntax nodes and inspects all implemented interfaces. It only triggers when a class implements two or more perspective interfaces (IPerspectiveFor<,> or IPerspectiveWithActionsFor<,>) and the first type argument (TModel) differs between them.

AOT Compatibility

The analyzer is fully AOT-compatible:

  • Runs at compile time via Roslyn's DiagnosticAnalyzer infrastructure
  • Uses INamedTypeSymbol.AllInterfaces for type discovery (no reflection)
  • Concurrent execution enabled for performance

See Also


Version 1.0.0 - Foundation Release