Create a custom action and event
Use case
Create a project-owned IGridAction when one logical
request must validate or mutate a single board in a way that no built-in action represents. Compose
built-in actions in GridBatchAction when they already express the required behavior.
Contract and ownership
Build receives a GridActionPlanBuilder
with a read-only view of the planned board and methods for recording mutations and events. Validate
all inputs before adding work. The action must not mutate live state or external objects while it
plans.
Events added to the plan are published only after a successful commit. A successful no-op publishes nothing; an event-only plan publishes a batch without advancing revision; a mutation advances the board once and publishes its batch.
Implementation
The example adds two adjacent cells atomically and publishes a project-owned BoardExpandedEvent.
Validate and describe the request
Excerpt — define the committed event. Tested using public APIs.
public sealed class BoardExpandedEvent : IGridEvent
{
public BoardExpandedEvent(GridCoordinate first, GridCoordinate second)
{
First = first ?? throw new ArgumentNullException(nameof(first));
Second = second ?? throw new ArgumentNullException(nameof(second));
}
public GridCoordinate First { get; }
public GridCoordinate Second { get; }
}
The request itself is an immutable value that implements IGridAction:
Excerpt — capture the requested coordinates. Tested using public APIs.
public sealed partial class ExpandBoardPairAction : IGridAction
{
public ExpandBoardPairAction(GridCoordinate first, GridCoordinate second)
{
First = first ?? throw new ArgumentNullException(nameof(first));
Second = second ?? throw new ArgumentNullException(nameof(second));
}
public GridCoordinate First { get; }
public GridCoordinate Second { get; }
}
Plan mutations and the committed event
Excerpt — add all work to the transaction builder. Tested using public APIs.
if (builder == null)
throw new ArgumentNullException(nameof(builder));
if (First.Equals(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"The two new cells must use different coordinates.",
First));
}
if (builder.State.ContainsCell(First) || builder.State.ContainsCell(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"Both coordinates must be absent before the board can expand."));
}
if (!builder.AddCell(First) || !builder.AddCell(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"The board could not add both cells to the same action plan."));
}
builder.AddEvent(new BoardExpandedEvent(First, Second));
return GridActionResult.SuccessResult(new[] { First, Second });
Preview and apply
Excerpt — preview without mutation and apply with fresh planning. Tested using public APIs.
public static (GridActionResult Preview, GridActionResult Applied) Run(
GridBoardState state,
GridCoordinate first,
GridCoordinate second)
{
IGridAction action = new ExpandBoardPairAction(first, second);
GridActionResult preview = GridActionRunner.Preview(state, action);
GridActionResult applied = preview.Success
? GridActionRunner.Apply(state, action)
: preview;
return (preview, applied);
}
Complete tested example
using System;
using LostLily.GridToolkit.Actions;
using LostLily.GridToolkit.Board;
using LostLily.GridToolkit.Coordinates;
using LostLily.GridToolkit.Events;
namespace GridToolkit.Tutorials
{
public sealed class BoardExpandedEvent : IGridEvent
{
public BoardExpandedEvent(GridCoordinate first, GridCoordinate second)
{
First = first ?? throw new ArgumentNullException(nameof(first));
Second = second ?? throw new ArgumentNullException(nameof(second));
}
public GridCoordinate First { get; }
public GridCoordinate Second { get; }
}
public sealed partial class ExpandBoardPairAction : IGridAction
{
public ExpandBoardPairAction(GridCoordinate first, GridCoordinate second)
{
First = first ?? throw new ArgumentNullException(nameof(first));
Second = second ?? throw new ArgumentNullException(nameof(second));
}
public GridCoordinate First { get; }
public GridCoordinate Second { get; }
}
public sealed partial class ExpandBoardPairAction
{
public GridActionResult Build(GridActionPlanBuilder builder)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
if (First.Equals(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"The two new cells must use different coordinates.",
First));
}
if (builder.State.ContainsCell(First) || builder.State.ContainsCell(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"Both coordinates must be absent before the board can expand."));
}
if (!builder.AddCell(First) || !builder.AddCell(Second))
{
return GridActionResult.Failure(new GridActionFailure(
GridActionFailureCode.InvalidAction,
"The board could not add both cells to the same action plan."));
}
builder.AddEvent(new BoardExpandedEvent(First, Second));
return GridActionResult.SuccessResult(new[] { First, Second });
}
}
public static class ExpandBoardPairWorkflow
{
public static (GridActionResult Preview, GridActionResult Applied) Run(
GridBoardState state,
GridCoordinate first,
GridCoordinate second)
{
IGridAction action = new ExpandBoardPairAction(first, second);
GridActionResult preview = GridActionRunner.Preview(state, action);
GridActionResult applied = preview.Success
? GridActionRunner.Apply(state, action)
: preview;
return (preview, applied);
}
}
}
Verification
Preview succeeds without changing revision, cells, or batches. Apply adds both cells, advances the
board once, and publishes one batch containing BoardExpandedEvent.
Tests
Also preview an invalid coordinate pair and verify that Apply is not attempted, no cell appears, revision remains unchanged, and no event is published.
Troubleshooting
| Symptom | Check |
|---|---|
| Preview changes state | Move all writes into transaction-builder mutations. |
| One of two cells appears after failure | Plan both mutations in the same action transaction. |
| An event arrives before state is readable | Add it to the plan rather than publishing from Build. |
| More than one board participates | Use a declared multi-board operation instead. |
Related
- Actions, transactions, and events
- Choose a built-in action
- Create multi-board operations
- IGridEvent
- GridActionPlanBuilder
Done when
The action plans only through public transaction contracts, rejection is immutable, and its event is observable only after a successful atomic commit.