Winche.Events.Commands 1.1.1

dotnet add package Winche.Events.Commands --version 1.1.1
                    
NuGet\Install-Package Winche.Events.Commands -Version 1.1.1
                    
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="Winche.Events.Commands" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Winche.Events.Commands" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Winche.Events.Commands" />
                    
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 Winche.Events.Commands --version 1.1.1
                    
#r "nuget: Winche.Events.Commands, 1.1.1"
                    
#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 Winche.Events.Commands@1.1.1
                    
#: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=Winche.Events.Commands&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Winche.Events.Commands&version=1.1.1
                    
Install as a Cake Tool

Winche.Events

A Marten-backed event sourcing library for .NET 10. Provides typed projections, an explicit unit-of-work session, optimistic concurrency, post-commit notifications, and an optional command-dispatch layer — all without exposing Marten types to your domain code.


Packages

Package Purpose
Winche.Events Core: event store, sessions, projections, notifiers
Winche.Events.Commands Optional: command handlers and dispatcher

Getting started

1. Define your events

All events must inherit from DomainEvent:

using Winche.Events.Models;

public record OrderPlaced(string OrderId, decimal Total) : DomainEvent;
public record OrderShipped(string OrderId) : DomainEvent;
public record OrderCancelled(string OrderId) : DomainEvent;

2. Define your aggregate and projection

using Winche.Events.Projection;

public record Order(string Id, string Status, decimal Total);

public class OrderProjection : Projection<Order>
{
    public override Order InitialState() => new(string.Empty, "none", 0);

    public Order Apply(Order state, OrderPlaced e)    => state with { Id = e.OrderId, Status = "placed",  Total = e.Total };
    public Order Apply(Order state, OrderShipped e)   => state with { Status = "shipped" };
    public Order Apply(Order state, OrderCancelled e) => state with { Status = "cancelled" };
}

Each Apply overload handles one event type. Unhandled event types are ignored — no fallback method needed.

3. Register services

using Winche.Events.DependencyInjection;
using Winche.Events.Projection;

services.AddWincheEvents(opts =>
{
    opts.ConnectionString = "Host=localhost;Database=mydb;Username=postgres;Password=...";

    opts.AddEventType<OrderPlaced>();
    opts.AddEventType<OrderShipped>();
    opts.AddEventType<OrderCancelled>();

    opts.AddProjection<OrderProjection, Order>(ProjectionMode.Live);
});

4. Use the event store

var store = provider.GetRequiredService<IEventStore>();

await using var session = await store.OpenSessionAsync();

await session.AppendAsync<Order>("orders/123", [new OrderPlaced("orders/123", 49.99m)]);
await session.SaveChangesAsync();

var order = await session.LoadAsync<Order>("orders/123");
// order.Status == "placed"

Projection modes

Mode Behaviour
Live Aggregate is computed on every LoadAsync by replaying the event stream. No stored document.
Inline Aggregate document is updated synchronously inside the same transaction when events are appended. LoadAsync is a simple document lookup.
Async Aggregate document is updated by a background daemon. Eventually consistent.

IEventSession

IEventSession is a unit of work scoped to a single PostgreSQL connection and transaction. Always dispose it with await using.

public interface IEventSession : IAsyncDisposable
{
    Task AppendAsync<TAggregate>(
        string streamId,
        IEnumerable<DomainEvent> events,
        long? expectedVersion = null,
        CancellationToken ct = default);

    Task<TAggregate?> LoadAsync<TAggregate>(
        string streamId,
        CancellationToken ct = default) where TAggregate : class;

    Task SaveChangesAsync(CancellationToken ct = default);
}

Optimistic concurrency — pass expectedVersion to AppendAsync to reject concurrent writes:

await session.AppendAsync<Order>("orders/123", events, expectedVersion: 3);

Marten throws if the stream's current version doesn't match.


Post-commit notifications

Implement IAppendNotifier to receive a callback after each successful commit:

using Winche.Events.Notification;

public class MyNotifier : IAppendNotifier
{
    public Task NotifyAsync(string streamId, string streamType,
        IReadOnlyList<DomainEvent> events, CancellationToken ct = default)
    {
        // Runs after the PostgreSQL transaction commits.
        // Events are already persisted — this cannot roll them back.
        return Task.CompletedTask;
    }
}

Register it:

opts.AddNotifier<MyNotifier>();

Multiple notifiers can be registered. Each runs independently; an exception in one is logged and swallowed and does not affect the others or the caller.


Commands (Winche.Events.Commands)

The commands package adds a load-handle-append-return dispatch loop on top of IEventSession.

1. Define commands and handlers

using Winche.Events.Commands;

public record PlaceOrderCommand(string OrderId, decimal Total);

public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand, Order>
{
    public Task<IEnumerable<DomainEvent>> HandleAsync(
        PlaceOrderCommand cmd, Order? state, CancellationToken ct = default)
    {
        if (state is { Status: not "none" })
            throw new InvalidOperationException("Order already exists.");

        return Task.FromResult<IEnumerable<DomainEvent>>(
            [new OrderPlaced(cmd.OrderId, cmd.Total)]);
    }
}

The state argument is the current aggregate loaded from the store (null if the stream does not exist yet).

2. Register

using Winche.Events.Commands.DependencyInjection;

services.AddWincheEventsCommands(commands =>
{
    commands.AddHandler<PlaceOrderCommand, Order, PlaceOrderHandler>();
});

3. Dispatch

var dispatcher = provider.GetRequiredService<ICommandDispatcher>();

var order = await dispatcher.DispatchAsync<PlaceOrderCommand, Order>(
    "orders/123", new PlaceOrderCommand("orders/123", 49.99m));

// order reflects the state after the command's events have been applied

Dispatch flow:

  1. Open a session
  2. Load current aggregate state
  3. Call handler → produce events
  4. Append events and commit
  5. Load and return updated state

Transaction isolation

OpenSessionAsync accepts an optional IsolationLevel:

await using var session = await store.OpenSessionAsync(IsolationLevel.Serializable);

Default is ReadCommitted.


Requirements

  • .NET 10
  • PostgreSQL (via Marten / Npgsql)
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
1.1.1 0 5/28/2026
1.1.0 0 5/28/2026
1.0.0 8 5/28/2026