Zooper.Tools.Marten.ApplyEnforcer.Contracts 2.0.0

dotnet add package Zooper.Tools.Marten.ApplyEnforcer.Contracts --version 2.0.0
                    
NuGet\Install-Package Zooper.Tools.Marten.ApplyEnforcer.Contracts -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Zooper.Tools.Marten.ApplyEnforcer.Contracts" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zooper.Tools.Marten.ApplyEnforcer.Contracts" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Zooper.Tools.Marten.ApplyEnforcer.Contracts" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Zooper.Tools.Marten.ApplyEnforcer.Contracts --version 2.0.0
                    
#r "nuget: Zooper.Tools.Marten.ApplyEnforcer.Contracts, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Zooper.Tools.Marten.ApplyEnforcer.Contracts@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Zooper.Tools.Marten.ApplyEnforcer.Contracts&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Zooper.Tools.Marten.ApplyEnforcer.Contracts&version=2.0.0
                    
Install as a Cake Tool

Zooper.Tools.Marten.ApplyEnforcer

Roslyn analyzers and source generators that enforce correct Marten event-sourcing patterns at compile time.

The Problem

Marten rebuilds aggregate state from events using convention-based methods (Create, Apply, ShouldDelete), but there's no compile-time enforcement. When a developer introduces a new domain event and forgets to add the corresponding handler on the aggregate, the projection silently produces incorrect state at runtime. Similarly, raw Marten append calls (AppendOne, AppendMany) bypass type safety entirely — nothing prevents appending events that don't belong to a given aggregate's stream.

The Solution

This toolkit provides zero-runtime-cost, compile-time enforcement through three components:

Component What it does
Source Generator Automatically discovers all events for an aggregate via IDomainEvent<TAggregate>
Coverage Analyzer Fails the build when an aggregate is missing a handler for a discovered event (MARTEN001)
Raw Append Analyzer Fails the build when code directly calls AppendOne/AppendMany outside an approved wrapper (MARTEN002)

No manual event lists. No runtime reflection. Just add a new event and the compiler tells you exactly what's missing.

Diagnostics

ID Severity Description
MARTEN001 Error Aggregate is missing a Marten convention handler for a discovered domain event
MARTEN002 Error Raw Marten append call used outside an approved wrapper class

Quick Start

1. Install the packages


<PackageReference Include="Zooper.Tools.Marten.ApplyEnforcer.Contracts" Version="1.0.0" />


<PackageReference Include="Zooper.Tools.Marten.ApplyEnforcer.Analyzers" Version="1.0.0"
                  PrivateAssets="all"
                  IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />

2. Declare your events

Events just need to implement IDomainEvent<TAggregate>. How you structure them is up to you:

// Simple records
public sealed record OrderCreated(Guid OrderId) : IDomainEvent<Order>;
public sealed record ItemAdded(string ItemName) : IDomainEvent<Order>;

// Versioned interfaces with nested concrete types
public interface IOrderCancelled : IDomainEvent<Order>
{
    public sealed record V1() : IOrderCancelled;
}

The generator discovers events by finding types that directly implement IDomainEvent<TAggregate>. If you use the versioned interface pattern, only the interface itself is counted — nested types like V1 are not, since they inherit IDomainEvent<T> transitively.

3. Write your aggregate with Create/Apply handlers

[EventSourcedAggregate]
public sealed record Order : IAggregateRoot<Guid>
{
    public Guid Id { get; init; }
    public List<string> Items { get; } = [];
    public bool IsCancelled { get; private set; }

    public static Order Create(OrderCreated domainEvent) => new();

    public void Apply(ItemAdded domainEvent)
    {
        Items.Add(domainEvent.ItemName);
    }

    public void Apply(IOrderCancelled domainEvent)
    {
        IsCancelled = true;
    }
}

The [EventSourcedAggregate] attribute is a one-time declaration — it never needs updating when new events are added.

If you now introduce a new IDomainEvent<Order> without adding a handler to Order, the build fails with MARTEN001.

4. Create a typed append wrapper

[ApprovedAppendWrapper]
public static class OrderStreamExtensions
{
    public static void AppendOrderEvent<TEvent>(
        this IEventBoundary<Order> stream,
        TEvent @event)
        where TEvent : IDomainEvent<Order>
    {
        stream.AppendOne(@event);
    }
}

The where TEvent : IDomainEvent<Order> constraint ensures only events that belong to the Order aggregate can be appended. The [ApprovedAppendWrapper] attribute tells the analyzer that raw Marten calls inside this class are permitted.

5. Use the typed wrapper

// ✅ Compiles — type-safe and enforced
eventStream.AppendOrderEvent(new IItemAdded.V1("Widget"));

// ❌ MARTEN002 — raw Marten append is forbidden
eventStream.AppendOne(new IItemAdded.V1("Widget"));

Supported Handler Conventions

The coverage analyzer recognizes the following Marten convention methods:

Method Signature Purpose
Create public static TAggregate Create(TEvent e) Initial aggregate creation from the first event
Apply public void Apply(TEvent e) State mutation from subsequent events
ShouldDelete public bool ShouldDelete(TEvent e) Deletion criteria for a given event

Contracts Reference

Type Kind Purpose
IDomainEvent<TAggregate> Interface Declares which aggregate an event belongs to
EventSourcedAggregateAttribute Attribute Marks a type as an aggregate requiring event coverage enforcement
ApprovedAppendWrapperAttribute Attribute Marks a class as an approved wrapper for raw Marten appends

How It Works

                    ┌──────────────────┐
                    │  IDomainEvent<T> │  ← Events declare aggregate affinity
                    └────────┬─────────┘
                             │
              ┌──────────────▼──────────────┐
              │   Source Generator (build)   │  ← Discovers all events per aggregate
              └──────────────┬──────────────┘
                             │
                   ┌─────────▼─────────┐
                   │  Generated Type[] │  ← Emitted metadata array
                   └─────────┬─────────┘
                             │
              ┌──────────────▼──────────────┐
              │   Coverage Analyzer         │  ← Checks aggregate has handler
              │   (MARTEN001)               │    for every discovered event
              └─────────────────────────────┘

              ┌─────────────────────────────┐
              │   Raw Append Analyzer       │  ← Flags direct AppendOne/AppendMany
              │   (MARTEN002)               │    calls outside approved wrappers
              └─────────────────────────────┘
  1. At build time, the source generator scans the compilation for all types that directly implement IDomainEvent<TAggregate> and emits a Type[] array per aggregate. Only direct implementors are counted — if you use versioned interfaces, nested types like V1 are automatically excluded.
  2. The coverage analyzer inspects every [EventSourcedAggregate]-decorated type and verifies it has a matching Create, Apply, or ShouldDelete handler for each discovered event. Missing handlers produce MARTEN001.
  3. The raw append analyzer inspects all method invocations and flags direct calls to AppendOne or AppendMany unless the containing type is marked with [ApprovedAppendWrapper]. Violations produce MARTEN002.

Requirements

  • .NET 10.0+ (for contracts and consumer projects)
  • C# with Roslyn support (any modern .NET SDK)

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 194 3/22/2026
1.0.1-dev.0.4 57 3/22/2026
1.0.1-dev.0.3 54 3/22/2026
1.0.1-dev.0.2 59 3/22/2026
1.0.0 108 3/22/2026