Table of Contents

Roslyn Analyzer Rules

The BookStore.ApiService.Analyzers project enforces architectural patterns for Event Sourcing, CQRS, and Domain-Driven Design in the backend API service.

Overview

The analyzer provides 14 rules across 4 categories to ensure consistent architecture:

  • Event Sourcing Rules (BS1xxx): Enforce event immutability and proper structure
  • CQRS Command Rules (BS2xxx): Ensure commands follow CQRS patterns
  • Aggregate Rules (BS3xxx): Validate Marten conventions and event sourcing patterns
  • Handler Rules (BS4xxx): Enforce Wolverine handler conventions

Rule Catalog

Event Sourcing Rules (BS1xxx)

BS1001: Events must be declared as record types

  • Severity: Warning
  • Category: EventSourcing

Events represent immutable historical facts and should use C# record types.

❌ Bad:

namespace BookStore.ApiService.Events;

public class BookAdded  // Should be a record
{
    public Guid Id { get; init; }
    public string Title { get; init; }
}

✅ Good:

namespace BookStore.ApiService.Events;

public record BookAdded(Guid Id, string Title);

BS1002: Event properties must be immutable

  • Severity: Warning
  • Category: EventSourcing

Event properties must not have mutable setters to preserve historical integrity.

❌ Bad:

namespace BookStore.ApiService.Events;

public record BookAdded
{
    public string Title { get; set; }  // Should use init
}

✅ Good:

namespace BookStore.ApiService.Events;

public record BookAdded(string Title);
// or
public record BookAdded
{
    public string Title { get; init; }
}

BS1003: Events must be in Events namespace

  • Severity: Warning
  • Category: Architecture

Events should be organized in namespaces ending with .Events for consistency.

❌ Bad:

namespace BookStore.ApiService.Models;

public record BookAdded(Guid Id);  // Should be in Events namespace

✅ Good:

namespace BookStore.ApiService.Events;

public record BookAdded(Guid Id);

CQRS Command Rules (BS2xxx)

BS2001: Commands must be declared as record types

  • Severity: Warning
  • Category: CQRS

Commands are immutable DTOs and should use C# record types.

❌ Bad:

namespace BookStore.ApiService.Commands.Books;

public class CreateBook  // Should be a record
{
    public string Title { get; init; }
}

✅ Good:

namespace BookStore.ApiService.Commands.Books;

public record CreateBook(string Title);

BS2002: Commands must be in the Commands namespace

  • Severity: Warning
  • Category: Architecture

Commands should be organized in namespaces ending with .Commands.

❌ Bad:

namespace BookStore.ApiService.Endpoints.Admin;

public record CreateBookRequest(string Title);  // Should be in Commands namespace

✅ Good:

namespace BookStore.ApiService.Commands.Books;

public record CreateBook(string Title);

BS2003: Command properties should use init accessors

  • Severity: Info (Suggestion)
  • Category: CQRS

Command properties should use init-only setters to ensure immutability after construction.

❌ Suboptimal:

namespace BookStore.ApiService.Commands.Books;

public record CreateBook
{
    public string Title { get; set; }  // Should use init
}

✅ Good:

namespace BookStore.ApiService.Commands.Books;

public record CreateBook
{
    public string Title { get; init; }
}

Aggregate Rules (BS3xxx)

BS3001: Apply methods must return void

  • Severity: Error
  • Category: EventSourcing

Marten requires Apply methods to return void for event application.

❌ Bad:

public class BookAggregate
{
    public BookAdded Apply(BookAdded @event)  // Should return void
    {
        Id = @event.Id;
        return @event;
    }
}

✅ Good:

public class BookAggregate
{
    void Apply(BookAdded @event)
    {
        Id = @event.Id;
    }
}

BS3002: Apply methods must have exactly one parameter

  • Severity: Error
  • Category: EventSourcing

Marten requires Apply methods to have exactly one parameter (the event).

❌ Bad:

public class BookAggregate
{
    void Apply(BookAdded @event, string reason)  // Too many parameters
    {
        Id = @event.Id;
    }
}

✅ Good:

public class BookAggregate
{
    void Apply(BookAdded @event)
    {
        Id = @event.Id;
    }
}

BS3003: Apply methods should be private or internal

  • Severity: Warning
  • Category: EventSourcing

Apply methods are called by Marten during rehydration and should not be public.

❌ Bad:

public class BookAggregate
{
    public void Apply(BookAdded @event)  // Should be private
    {
        Id = @event.Id;
    }
}

✅ Good:

public class BookAggregate
{
    void Apply(BookAdded @event)  // private by default
    {
        Id = @event.Id;
    }
}

BS3004: Aggregate command methods should return events

  • Severity: Warning
  • Category: EventSourcing

Aggregate command methods generate events for event sourcing and should return event types.

❌ Bad:

public class BookAggregate
{
    public void UpdateTitle(string title)  // Should return event
    {
        // ...
    }
}

✅ Good:

public class BookAggregate
{
    public BookTitleUpdated UpdateTitle(string title)
    {
        return new BookTitleUpdated(Id, title);
    }
}

BS3005: Aggregate properties should not have public setters

  • Severity: Warning
  • Category: DomainModel

Aggregate state changes should only occur through Apply methods, not direct property setters.

❌ Bad:

public class BookAggregate
{
    public Guid Id { get; set; }  // Should use init or private set
    public string Title { get; set; }
}

✅ Good:

public class BookAggregate
{
    public Guid Id { get; set; }  // Marten needs this for rehydration
    public string Title { get; private set; } = string.Empty;
    
    void Apply(BookTitleUpdated @event)
    {
        Title = @event.Title;  // State changes through Apply
    }
}

Handler Convention Rules (BS4xxx)

BS4001: Handler methods should be named 'Handle'

  • Severity: Info (Suggestion)
  • Category: CQRS

Wolverine discovers handlers by the method name Handle.

❌ Suboptimal:

public static class BookHandlers
{
    public static IResult ProcessCreateBook(CreateBook cmd)  // Should be named Handle
    {
        // ...
    }
}

✅ Good:

public static class BookHandlers
{
    public static IResult Handle(CreateBook cmd)
    {
        // ...
    }
}

BS4002: Handler methods should be static

  • Severity: Warning
  • Category: CQRS

Static handler methods provide better performance in Wolverine.

❌ Bad:

public class BookHandlers
{
    public IResult Handle(CreateBook cmd)  // Should be static
    {
        // ...
    }
}

✅ Good:

public static class BookHandlers
{
    public static IResult Handle(CreateBook cmd)
    {
        // ...
    }
}

BS4003: Handler first parameter should be a command type

  • Severity: Info (Suggestion)
  • Category: CQRS

Wolverine routes messages based on the first parameter type, which should be from a .Commands namespace.

❌ Suboptimal:

public static IResult Handle(string bookId)  // Should accept a command
{
    // ...
}

✅ Good:

public static IResult Handle(CreateBook cmd)
{
    // ...
}

Configuration

Adjusting Rule Severity

You can configure rule severities in .editorconfig:

# Make BS2002 an error instead of warning
dotnet_diagnostic.BS2002.severity = error

# Disable BS4001 if you prefer different handler naming
dotnet_diagnostic.BS4001.severity = none

Suppressing Rules

For specific cases where you need to suppress a rule:

#pragma warning disable BS2002
public record SpecialRequest(string Data);  // Not in Commands namespace for a reason
#pragma warning restore BS2002

Or use attributes:

[System.Diagnostics.CodeAnalysis.SuppressMessage("Architecture", "BS2002")]
public record SpecialRequest(string Data);

Benefits

Consistent Architecture: Enforces Event Sourcing and CQRS patterns across the codebase
Early Detection: Catches architectural violations during development
Team Alignment: Helps new developers follow established patterns
Reduced Code Review: Automated checks reduce manual review burden
IDE Integration: Real-time feedback in Visual Studio, VS Code, and Rider


Testing

The analyzer includes comprehensive unit tests using actual C# files (not strings) for better maintainability. Tests are organized in TestData folders by diagnostic ID.

Run tests:

dotnet test --project src/BookStore.ApiService.Analyzers.Tests/BookStore.ApiService.Analyzers.Tests.csproj

Further Reading