Follow this guide to create a new event-sourced aggregate in the ApiService following strict Event Sourcing patterns.
- •
Define the Initial Event
- •Create a
recordinsrc/BookStore.ApiService/Events/ - •Naming: Past tense (e.g.,
AuthorCreated, notCreateAuthor) - •Properties: Include all initial state properties
- •IDs: Use
Guidfor aggregate ID - •Timestamp: Use
DateTimeOffset - •Example:
csharp
namespace BookStore.ApiService.Events; public record AuthorCreated( Guid Id, string Name, string Biography, DateTimeOffset CreatedAt );
- •Create a
- •
Create the Aggregate
- •Create a
recordinsrc/BookStore.ApiService/Aggregates/ - •Template:
csharp
namespace BookStore.ApiService.Aggregates; public record Author { public Guid Id { get; init; } public string Name { get; init; } = string.Empty; public string Biography { get; init; } = string.Empty; public bool Deleted { get; init; } public int Version { get; init; } // Factory method for new aggregates public static Author Create(AuthorCreated @event) { return new Author { Id = @event.Id, Name = @event.Name, Biography = @event.Biography }; } // Apply method for Marten (MUST be void, single parameter) public void Apply(AuthorCreated @event) { // Marten uses this for event replay - do not return anything } // Apply method for subsequent events public void Apply(AuthorUpdated @event) { // Handle state changes } }
- •Create a
- •
Add Behavior Methods
- •Add methods to aggregate that return events
- •Pattern: Validate → Return Event
- •Example:
csharp
public AuthorUpdated Update(string name, string biography) { if (Deleted) throw new InvalidOperationException("Cannot update deleted author"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name is required", nameof(name)); return new AuthorUpdated(Id, name, biography, DateTimeOffset.UtcNow); }
- •
Configure Marten
- •Open
src/BookStore.ApiService/Program.cs - •Add aggregate to Marten's event store:
csharp
builder.Services.AddMarten(options => { // Existing configuration... // Add your aggregate options.Events.StreamIdentity = StreamIdentity.AsGuid; });
- •Open
- •
Create Unit Tests
- •Create test file in
tests/BookStore.ApiService.UnitTests/Aggregates/ - •Test Pattern:
csharp
using TUnit.Core; using TUnit.Assertions.Extensions; public class AuthorTests { [Test] public async Task Create_ReturnsValidAggregate() { // Arrange var created = new AuthorCreated( Guid.CreateVersion7(), "Martin Fowler", "Author and speaker", DateTimeOffset.UtcNow ); // Act var author = Author.Create(created); // Assert await Assert.That(author.Id).IsEqualTo(created.Id); await Assert.That(author.Name).IsEqualTo("Martin Fowler"); } [Test] public async Task Update_DeletedAuthor_ThrowsException() { // Arrange var author = new Author { Deleted = true }; // Act & Assert await Assert.That(() => author.Update("New Name", "Bio")) .Throws<InvalidOperationException>(); } }
- •Create test file in
- •
Verify Analyzer Compliance
- •Run
dotnet buildto check for BS1xxx-BS4xxx warnings - •Ensure:
- •✅ Events are
recordtypes (BS1001) - •✅ Apply methods are
voidwith single parameter (BS1002) - •✅ Aggregates use proper patterns (BS2xxx)
- •✅ Events are
- •Run
- •
Next Steps
- •Use
/scaffold-projectionto create read models from your aggregate's events - •Use
/scaffold-writeto create complete command/handler/endpoint flow - •Use
/scaffold-testto create integration tests - •Use
/verify-featureto ensure everything works
- •Use
Related Skills
Prerequisites:
- •None - this is a foundational skill for event sourcing
Next Steps:
- •
/scaffold-projection- Create read models from aggregate events - •
/scaffold-write- Add commands, handlers, and endpoints for this aggregate - •
/scaffold-test- Create integration tests - •
/verify-feature- Run all verification checks
See Also:
- •scaffold-write - Complete write operation workflow
- •event-sourcing-guide - Event Sourcing patterns
- •marten-guide - Marten event store integration
- •analyzer-rules - Code analyzer rules (BS1xxx-BS4xxx)
- •ApiService AGENTS.md - Backend patterns and conventions
Key Rules to Remember
- •Apply Methods: MUST be
voidwith single event parameter (Marten convention) - •Behavior Methods: Return events, don't mutate state directly
- •IDs: Use
Guid.CreateVersion7()for new aggregates - •Immutability: Use
recordtypes withinitproperties - •Validation: Validate in behavior methods, throw exceptions for invalid operations