| name | state-change-slice |
| description | Generate commands, events, aggregates, Wolverine HTTP endpoints (POST), and tests for STATE_CHANGE slices |
STATE_CHANGE Slice Skill
Overview
A STATE_CHANGE slice modifies aggregate state through commands and events. It generates:
- Command record (in
Domain/Commands)
- Event record (in
Events)
- Aggregate with event sourcing (in
Domain)
- Wolverine HTTP endpoint (POST) (in
<Context>/Slices/<SliceName>)
- Test cases (from specifications)
Note: STATE_CHANGE slices do NOT generate projections. Those belong to STATE_VIEW slices.
Flow Pattern
HTTP POST → Command → Aggregate (Wolverine handler) → Event appended to Marten stream
Java → C# / Critter Stack Mapping
| Java / Axon | C# / Critter Stack |
|---|
@CommandHandler on Aggregate | Handle(Command, IEventStream<T>) returning events |
@EventSourcingHandler | Apply(Event e) methods on aggregate |
CommandGateway.sendAndWait() | Wolverine HTTP endpoint cascade |
AggregateLifecycle.apply() | stream.AppendOne(event) |
AggregateTestFixture | Wolverine TrackedSession + Alba |
CommandResult(id, version) | CommandResult(Guid, long) |
Code Generation Steps
Step 1: Create Command Record
Location: Domain/Commands/<CommandName>.cs
using SlicingWorkshop.Common;
namespace SlicingWorkshop.Domain.Commands;
public record <CommandName>(
Guid AggregateId
// Add fields from slice command definition
) : ICommand;
Step 2: Create Event Record
Location: Events/<EventName>.cs
using SlicingWorkshop.Common;
namespace SlicingWorkshop.Events;
public record <EventName>(
Guid AggregateId
// Add fields from slice event definition
) : IEvent;
Step 3: Add Handler to Aggregate
Location: Domain/<Aggregate>.cs
For new aggregate (createsAggregate: true or first command):
using Marten;
using Marten.Events;
using SlicingWorkshop.Common;
using SlicingWorkshop.Domain.Commands;
using SlicingWorkshop.Events;
namespace SlicingWorkshop.Domain;
public class <Aggregate>
{
public Guid Id { get; private set; }
public static IEnumerable<IEvent> Handle(<CommandName> command, IEventStream<<Aggregate>> stream)
{
yield return new <EventName>(
command.AggregateId
// Map command fields to event fields
);
}
public void Apply(<EventName> @event)
{
Id = @event.AggregateId;
}
}
For existing aggregate:
public IEnumerable<IEvent> Handle(<CommandName> command, IEventStream<<Aggregate>> stream)
{
yield return new <EventName>(
command.AggregateId
// Map command fields to event fields
);
}
Step 4: Create Wolverine HTTP Endpoint
Location: <Context>/Slices/<SliceName>/<SliceName>Endpoint.cs
using Marten;
using SlicingWorkshop.Common;
using SlicingWorkshop.Domain.Commands;
using Wolverine.Http;
using Wolverine.Marten;
namespace SlicingWorkshop.<Context>.Slices.<SliceName>;
public static class <SliceName>Endpoint
{
[WolverinePost("/api/<context>/<resource>")]
public static async Task<CommandResult> Post(
<CommandName> command,
IDocumentSession session)
{
var stream = await session.Events.FetchForWritingAsync<<Aggregate>>(command.AggregateId);
var @event = <Aggregate>.Handle(command, stream);
stream.AppendMany(@event);
await session.SaveChangesAsync();
return new CommandResult(command.AggregateId, stream.CurrentVersion);
}
}
Note: When using [AggregateHandler] pattern (recommended for Wolverine+Marten):
[WolverinePost("/api/<context>/<resource>")]
public static (CommandResult, <EventName>) Post(
<CommandName> command,
[Aggregate] <Aggregate> aggregate)
{
var @event = new <EventName>(command.AggregateId );
return (new CommandResult(command.AggregateId, 0), @event);
}
Step 5: Create Tests
Location: <Context>/Slices/<SliceName>/Tests/<CommandName>Tests.cs
using Alba;
using Marten;
using SlicingWorkshop.Events;
using Xunit;
namespace SlicingWorkshop.<Context>.Slices.<SliceName>.Tests;
public class <CommandName>Tests : IClassFixture<AppFixture>
{
private readonly AppFixture _fixture;
public <CommandName>Tests(AppFixture fixture) => _fixture = fixture;
[Fact]
public async Task <SpecTitleAsPascalCase>()
{
var aggregateId = Guid.NewGuid();
await _fixture.Host.Scenario(x =>
{
x.Post.Json(new { AggregateId = aggregateId })
.ToUrl("/api/<context>/<resource>");
x.StatusCodeShouldBe(200);
});
using var session = _fixture.Store.LightweightSession();
var events = await session.Events.FetchStreamAsync(aggregateId);
Assert.Contains(events, e => e.Data is <EventName> ev && ev.AggregateId == aggregateId);
}
}
Conventions
- Commands are records implementing
ICommand
- Events are records implementing
IEvent
- Name commands in imperative form:
RegisterCustomer, not CustomerRegistrationRequest
- One file per slice artifact
- Throw exceptions for business rule violations — Wolverine will return 400