Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/guide/durability/marten/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,93 @@ type of `IMartenOp` is a side effect.
Like any other "side effect", you could technically return this as the main return type of a method or as part of a
tuple.

## Data Requirements <Badge type="tip" text="5.13" />

Wolverine provides declarative data requirement checks that verify whether a Marten document exists (or does not
exist) before a handler or HTTP endpoint executes. If the check fails, a `RequiredDataMissingException` is thrown,
preventing the handler from running.

### Using Attributes

The simplest way to declare data requirements is with the `[DocumentExists<T>]` and `[DocumentDoesNotExist<T>]`
attributes on handler methods:

```csharp
// Convention: looks for a property named "UserId" or "Id" on the command
[DocumentExists<User>]
public void Handle(PromoteUser command)
{
// Only runs if a User document with the matching identity exists
}

// Explicit property name for the identity
[DocumentDoesNotExist<User>(nameof(AddUser.UserId))]
public void Handle(AddUser command)
{
// Only runs if no User document with the matching identity exists
}
```

The identity property is resolved from the message/request type by convention:
1. If a property name is specified explicitly in the attribute constructor, that is used
2. Otherwise, Wolverine looks for a property named `{DocumentTypeName}Id` (e.g., `UserId` for `User`)
3. As a fallback, Wolverine looks for a property named `Id`

You can apply multiple attributes to a single handler method to check multiple documents:

```csharp
[DocumentExists<Department>(nameof(TransferEmployee.TargetDepartmentId))]
[DocumentExists<Employee>]
public void Handle(TransferEmployee command)
{
// Only runs if both the employee and target department exist
}
```

### Using the Before Method Pattern

For more complex requirements, or when you need access to the command properties at runtime to construct
the check, use the `Before` method pattern with `MartenOps.Document<T>()`:

```csharp
public static class CreateThingHandler
{
// Single requirement
public static IMartenDataRequirement Before(CreateThing command)
=> MartenOps.Document<ThingCategory>().MustExist(command.Category);

public static IMartenOp Handle(CreateThing command)
{
return MartenOps.Store(new Thing
{
Id = command.Name,
CategoryId = command.Category
});
}
}

public static class CreateThing2Handler
{
// Multiple requirements
public static IEnumerable<IMartenDataRequirement> Before(CreateThing2 command)
{
yield return MartenOps.Document<ThingCategory>().MustExist(command.Category);
yield return MartenOps.Document<Thing>().MustNotExist(command.Name);
}

public static IMartenOp Handle(CreateThing2 command)
{
return MartenOps.Store(new Thing
{
Id = command.Name,
CategoryId = command.Category
});
}
}
```

When multiple data requirements are present in the same handler (whether from attributes or `Before` methods),
Wolverine will automatically batch the existence checks into a single Marten batch query for efficiency.



24 changes: 14 additions & 10 deletions src/Persistence/MartenTests/Dcb/University/ChangeCourseCapacity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static async Task Handle(ChangeCourseCapacity command, IDocumentSession s
var query = new EventTagQuery()
.Or<CourseCreated, CourseId>(command.CourseId)
.Or<CourseCapacityChanged, CourseId>(command.CourseId);

var boundary = await session.Events.FetchForWritingByTags<CourseState>(query);

var state = boundary.Aggregate;
Expand All @@ -44,15 +45,19 @@ public class State
public bool Created { get; private set; }
public int Capacity { get; private set; }

public void Apply(CourseCreated e)
public void Evolve(IEvent e)
{
Created = true;
Capacity = e.Capacity;
}

public void Apply(CourseCapacityChanged e)
{
Capacity = e.Capacity;
switch (e.Data)
{
case CourseCreated c:
Created = true;
Capacity = c.Capacity;
break;

case CourseCapacityChanged changed:
Capacity = changed.Capacity;
break;
}
}
}

Expand All @@ -75,7 +80,7 @@ public static HandlerContinuation Validate(
return HandlerContinuation.Continue;
}

public static CourseCapacityChanged? Handle(ChangeCourseCapacity command, State state)
public static CourseCapacityChanged? Handle(ChangeCourseCapacity command, [BoundaryModel]State state)
{
return command.Capacity != state.Capacity
? new CourseCapacityChanged(FacultyId.Default, command.CourseId, command.Capacity)
Expand Down Expand Up @@ -125,7 +130,6 @@ public static HandlerContinuation Validate(

public static CourseCapacityChanged? Handle(ChangeCourseCapacity command,

// TODO -- see if we could auto-register this with Marten?
[WriteAggregate]
Course state)
{
Expand Down
Loading
Loading