Create multi-board operations
Use case
Use a project-owned IGridOperation when one
logical command must coordinate mutations or events across more than one board. Use an ordinary
IGridAction when only one board participates.
Contract and ownership
Declare every participant before planning. One callback builds all participant plans, and GridMultiBoardActionRunner rejects the whole operation when any participant fails. Preview and Apply return separate result objects; the caller owns those results.
Only mutated boards advance revision. Event-only participants publish at their unchanged revision,
and no-op participants publish nothing.
The example's SecondBoardCommitKind makes those three participant outcomes explicit.
Implementation
Declare participants and ownership
Excerpt — identify every board before planning. Tested using public APIs.
private static GridBoardState[] GetParticipants(
GridBoardState first,
GridBoardState second)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
throw new ArgumentNullException(nameof(second));
if (ReferenceEquals(first, second))
{
throw new ArgumentException(
"The tutorial operation requires two different board owners.",
nameof(second));
}
return new[] { first, second };
}
Build participant-specific plans
Excerpt — plan the first board's mutation and event. Tested using public APIs.
if (!builder.ForBoard(first).AddCell(operation.FirstCoordinate))
{
return new MultiBoardExpansionResult(
false,
"The first board rejected its planned cell.");
}
builder.ForBoard(first).AddEvent(new MultiBoardExpansionEvent(
operation.FirstCoordinate,
operation.SecondBoardCommit));
The second participant can mutate, publish an event without mutation, or remain a no-op:
Excerpt — plan the second board according to the operation. Tested using public APIs.
GridActionPlanBuilder secondBuilder = builder.ForBoard(second);
switch (operation.SecondBoardCommit)
{
case SecondBoardCommitKind.Mutation:
if (!secondBuilder.AddCell(operation.SecondCoordinate))
{
return new MultiBoardExpansionResult(
false,
"The second board rejected its planned cell.");
}
break;
case SecondBoardCommitKind.EventOnly:
secondBuilder.AddEvent(new MultiBoardExpansionEvent(
operation.SecondCoordinate,
operation.SecondBoardCommit));
break;
case SecondBoardCommitKind.NoOp:
break;
default:
return new MultiBoardExpansionResult(
false,
"The second-board commit kind is not supported.");
}
Preview and apply
Excerpt — preview without mutation. Tested using public APIs.
public static MultiBoardExpansionResult Preview(
GridBoardState first,
GridBoardState second,
MultiBoardExpansionOperation operation)
{
GridBoardState[] participants = GetParticipants(first, second);
if (operation == null)
throw new ArgumentNullException(nameof(operation));
return GridMultiBoardActionRunner.Preview(
participants,
operation,
builder => Plan(first, second, operation, builder));
}
Excerpt — apply with fresh planning. Tested using public APIs.
public static MultiBoardExpansionResult Apply(
GridBoardState first,
GridBoardState second,
MultiBoardExpansionOperation operation)
{
GridBoardState[] participants = GetParticipants(first, second);
if (operation == null)
throw new ArgumentNullException(nameof(operation));
return GridMultiBoardActionRunner.Apply(
participants,
operation,
builder => Plan(first, second, operation, builder));
}
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 enum SecondBoardCommitKind
{
Mutation,
EventOnly,
NoOp
}
public sealed class MultiBoardExpansionEvent : IGridEvent
{
public MultiBoardExpansionEvent(
GridCoordinate coordinate,
SecondBoardCommitKind secondBoardCommit)
{
Coordinate = coordinate ??
throw new ArgumentNullException(nameof(coordinate));
SecondBoardCommit = secondBoardCommit;
}
public GridCoordinate Coordinate { get; }
public SecondBoardCommitKind SecondBoardCommit { get; }
}
public sealed class MultiBoardExpansionOperation : IGridOperation
{
public MultiBoardExpansionOperation(
GridCoordinate firstCoordinate,
GridCoordinate secondCoordinate,
SecondBoardCommitKind secondBoardCommit =
SecondBoardCommitKind.Mutation)
{
FirstCoordinate = firstCoordinate ??
throw new ArgumentNullException(nameof(firstCoordinate));
SecondCoordinate = secondCoordinate ??
throw new ArgumentNullException(nameof(secondCoordinate));
SecondBoardCommit = secondBoardCommit;
}
public GridCoordinate FirstCoordinate { get; }
public GridCoordinate SecondCoordinate { get; }
public SecondBoardCommitKind SecondBoardCommit { get; }
}
public sealed class MultiBoardExpansionResult : IGridOperationResult
{
public MultiBoardExpansionResult(bool success, string message)
{
Success = success;
Message = message ?? string.Empty;
}
public bool Success { get; }
public string Message { get; }
}
public static class MultiBoardExpansionWorkflow
{
public static MultiBoardExpansionResult Preview(
GridBoardState first,
GridBoardState second,
MultiBoardExpansionOperation operation)
{
GridBoardState[] participants = GetParticipants(first, second);
if (operation == null)
throw new ArgumentNullException(nameof(operation));
return GridMultiBoardActionRunner.Preview(
participants,
operation,
builder => Plan(first, second, operation, builder));
}
public static MultiBoardExpansionResult Apply(
GridBoardState first,
GridBoardState second,
MultiBoardExpansionOperation operation)
{
GridBoardState[] participants = GetParticipants(first, second);
if (operation == null)
throw new ArgumentNullException(nameof(operation));
return GridMultiBoardActionRunner.Apply(
participants,
operation,
builder => Plan(first, second, operation, builder));
}
private static MultiBoardExpansionResult Plan(
GridBoardState first,
GridBoardState second,
MultiBoardExpansionOperation operation,
GridMultiBoardPlanBuilder builder)
{
if (!builder.ForBoard(first).AddCell(operation.FirstCoordinate))
{
return new MultiBoardExpansionResult(
false,
"The first board rejected its planned cell.");
}
builder.ForBoard(first).AddEvent(new MultiBoardExpansionEvent(
operation.FirstCoordinate,
operation.SecondBoardCommit));
GridActionPlanBuilder secondBuilder = builder.ForBoard(second);
switch (operation.SecondBoardCommit)
{
case SecondBoardCommitKind.Mutation:
if (!secondBuilder.AddCell(operation.SecondCoordinate))
{
return new MultiBoardExpansionResult(
false,
"The second board rejected its planned cell.");
}
break;
case SecondBoardCommitKind.EventOnly:
secondBuilder.AddEvent(new MultiBoardExpansionEvent(
operation.SecondCoordinate,
operation.SecondBoardCommit));
break;
case SecondBoardCommitKind.NoOp:
break;
default:
return new MultiBoardExpansionResult(
false,
"The second-board commit kind is not supported.");
}
return new MultiBoardExpansionResult(true, string.Empty);
}
private static GridBoardState[] GetParticipants(
GridBoardState first,
GridBoardState second)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
throw new ArgumentNullException(nameof(second));
if (ReferenceEquals(first, second))
{
throw new ArgumentException(
"The tutorial operation requires two different board owners.",
nameof(second));
}
return new[] { first, second };
}
}
}
Verification
Preview leaves every participant unchanged. A successful Apply commits all accepted plans as one operation. A rejected participant leaves every board unchanged, with no partial revisions or event batches.
Tests
Test a mutation participant, an event-only participant, a no-op participant, and a rejection after another participant has already planned work.
Troubleshooting
| Symptom | Check |
|---|---|
| One board was not considered | Declare it before the shared planning callback runs. |
| An event-only board advances revision | Do not add a mutation to that participant's plan. |
| Partial state appears after rejection | Keep all participant changes inside the runner's plans rather than mutating external state. |
Related
- Create a custom action and event
- Batch actions on one board
- Actions, transactions, and events
- IGridOperationResult
Done when
Preview is immutable, Apply commits all declared participants atomically, per-board revisions match their mutations, and rejection cannot expose partial state.