Lost Lily/Grid ToolkitDocs 1.0
Table of Contents

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.

Done when

Preview is immutable, Apply commits all declared participants atomically, per-board revisions match their mutations, and rejection cannot expose partial state.