Skip to content

2. Getting Started

Guide Home | Previous: 1. Overview | Next: 3. Core Concepts

View in another language — C#/.NET · C++ · Java · Kotlin · Node/TypeScript

The document that owns this chapter's contract — none. This is a walkthrough for installing and confirming your first working setup.

First install the package and run a minimal example of two processes calling each other (§1-§2), then follow the actual TicTacToe sample through the flow of creating one room (§3-§11).

1. Installation

Get it from NuGet. The minimal combination needed to build one server consists of these three packages.

# The core messaging engine (.NET binding)
dotnet add package Zlink
# The contract and runtime
dotnet add package Zlink.Framework
# DI/hosted service registration (AddZLinkFramework)
dotnet add package Zlink.Framework.AspNetCore

Packages to add when you need them:

Package When to add it
Zlink.Framework.Locations.Redis When using the Redis location store for auto-connect (10-location)
Zlink.Framework.Codecs.Protobuf · .MessagePack To use instead of the default JSON codec (05-channel-messaging §7)
Zlink.Stream.Connector When building an external client (a game client, mobile) (09-stream)
Zlink.HttpClient When the server calls out over HTTP (HTTP Client guide)

Framework packages ship starting at 0.9. Zlink (the core binding package) and Zlink.HttpClient follow their own version tracks, so the three packages' version numbers differ. net8.0 or later is required.

Core, bindings, and framework packages are distributed under MPL-2.0. There's no cost to building and selling a service (17-alternative §7).

2. A Minimal Example — Two Processes Calling Each Other

With no location store and no Redis, try one request/reply over a manual connection with the endpoint specified directly. This confirms that installation is complete.

The shared contract. Both processes reference the same record.

public sealed record Hello(string Name);
public sealed record Greeting(string Text);

The server process. Owns the greeting channel and registers a handler.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddZLinkFramework(options =>
{
    // Finds handler types.
    options.AddHandlersFromAssemblyOf<Program>();

    // Names the mesh.
    var mesh = options.AddRouteMesh("services")
        // Its own endpoint for other processes to connect to.
        .Listen("tcp://0.0.0.0:7101");
    // This process handles "greeting".
    mesh.Channel("greeting").Server();
});

var app = builder.Build();
await app.RunAsync();

// A handler that processes one request.
public sealed class HelloHandler : IZLinkRequestHandler<Hello, Greeting>
{
    public ValueTask<Greeting> HandleAsync(
        Hello request,
        IZLinkMessageContext context,
        CancellationToken cancellationToken)
        => ValueTask.FromResult(new Greeting($"hello, {request.Name}"));
}

The client process. Joins the same mesh and calls greeting.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddZLinkFramework(options =>
{
    // It also needs its own endpoint.
    var mesh = options.AddRouteMesh("services").Listen("tcp://0.0.0.0:7102");
    // The call-only side is Client.
    mesh.Channel("greeting").Client();
    // Manual connection — write the server endpoint directly.
    mesh.PeerConnections.Connect("tcp://127.0.0.1:7101");
});

var app = builder.Build();

app.MapGet("/hello/{name}", async (
    string name,
    IZLinkRouteClient route,
    CancellationToken cancellationToken) =>
{
    // The target is just one ChannelName. Which node handles it isn't specified.
    var reply = await route
        .RequestToChannel("greeting", new Hello(name))
        .Async<Greeting>(cancellationToken);

    return Results.Ok(reply.Text);
});

await app.RunAsync();

Start the server first, then the client, and call curl http://localhost:5000/hello/world — it returns hello, world.

This confirms three things: the package is wired up, the two processes are connected through the mesh, and the call was routed by logical name (greeting) alone. This example has no Redis and no location store. For the calling code to stay the same as servers scale up and down, you need auto-connect, which is covered by 10-location.

3. TicTacToe — The Flow of Creating One Room

From here on, we'll use an actual sample. The API server doesn't pick a specific Play node — it only passes the room's stable type and its initial settings. The Framework selects one of the Object Servers that registered that type, and issues a globally unique SpotId.

3.1 Execution Flow

↗ View larger

The API code never carries the Play node's NodeRid or endpoint. The same creation code is used even as Play nodes are added or replaced.

3.2 Sample Locations

What to check File
Full run samples/TicTacToe/run_sample.sh
API run project samples/TicTacToe/Server/Api/TicTacToe.Server.Api.csproj
Play run project samples/TicTacToe/Server/Play/TicTacToe.Server.Play.csproj
HTTP handler samples/TicTacToe/Server/Api/Handlers/CreateGameHttpHandler.cs
API Framework config samples/TicTacToe/Server/Api/ApiServer.cs
Play Framework config samples/TicTacToe/Server/Play/PlayServer.cs
Game Spot samples/TicTacToe/Server/Play/Infrastructure/ZLink/Spots/TicTacToeGameSpot/TicTacToeGame.cs
Shared messages samples/TicTacToe/Shared/Contracts/Messages.cs

The table's relative paths are rooted at framework/languages/dotnet.

4. API Server Configuration

The API server registers a Location Store and an Object Client role. The Object Client role is used to create or call Actors and Spots on another Object Server.

builder.Services.AddZLinkFramework(options =>
{
    // Registers a shared Store so every process queries the same location information.
    options.AddLocationStore(new ZLinkRedisLocationStore(redis =>
    {
        redis.ConnectionString = settings.RedisEndpoint;
        redis.KeyPrefix = settings.RedisKeyPrefix;
    }));

    var mesh = options.AddRouteMesh(SampleNodes.Mesh)
        .Listen(settings.MeshEndpoint)
        .SetRoutingIdPrefix("tictactoe-api");

    // The API process doesn't hold any Object — it only initiates remote Object calls.
    mesh.Objects().Client();
});

The sample reads the peer endpoint from a config file for reproducible local runs. This endpoint only sets up the connection — it doesn't specify which Play node the new Game Spot gets placed on.

5. Creating a Spot from an HTTP Request

The HTTP handler uses the spot manager it received through DI.

internal static async Task<IResult> HandleAsync(
    CreateGameHttpReq request,
    IZLinkSpotManager spots,
    SampleSettings settings,
    ILoggerFactory loggerFactory,
    CancellationToken cancellationToken)
{
    var gameName = !string.IsNullOrWhiteSpace(request.GameName)
        ? request.GameName
        : SampleDefaults.GameName;

    var created = await spots
        // A node that provides this stable type becomes a candidate.
        .Create(SampleTypes.GameSpot)
        // Selects the RouteMesh to create the Object on.
        .InMesh(SampleNodes.Mesh)
        .Request(new TicTacToeGameCreateReq(
            gameName,
            // The initial settings passed to the new Spot's OnCreateAsync.
            SampleDefaults.RequiredLevel))
        .Async(cancellationToken);

    return Results.Ok(new CreateGameHttpRes(
        // Uses the Framework-issued SpotId as the room id.
        created.Spot.SpotId,
        settings.PlayEndpoints,
        settings.PlayNodes,
        gameName,
        SampleDefaults.RequiredLevel));
}

Use Create to create a new User Spot where the caller doesn't decide the SpotId. To look up or create the same SpotId again, use GetOrCreate(spotId, spotType).

6. Registering a Stable Type on the Play Server

The Framework considers only Serving Object Servers that have registered the requested stable type as creation candidates. The Play server registers the TicTacToeGame factory as follows.

var mesh = options.AddRouteMesh(SampleNodes.Mesh)
    .Listen(settings.MeshEndpoint)
    .SetRoutingIdPrefix("tictactoe-play");

mesh.Objects().Server()
    .AddSpotFactory<TicTacToeGame>(
        // The same stable type the API passed to Create.
        SampleTypes.GameSpot,
        factory => factory.DisableRelocation());

The sample does not define a contract for preferring a specific Play node or placing by NodeRid. The Framework and Location Store decide the placement candidate and capacity.

7. Validating the Initial Settings

The selected Play node creates the Spot, then hands the initial request to OnCreateAsync. The Spot validates the settings and returns whether it accepts creation.

public ValueTask<ZLinkSpotCreateResponse> OnCreateAsync(
    ZLinkMessage request,
    CancellationToken cancellationToken)
{
    var settings = request.Decode<TicTacToeGameCreateReq>();

    if (string.IsNullOrWhiteSpace(settings.GameName))
        return ValueTask.FromResult(
            ZLinkSpotCreateResponse.Reject("GameName is required."));

    _gameName = settings.GameName;
    _requiredLevel = settings.RequiredLevel;

    // Only after Accept is this Spot published as Ready in the Location Store.
    return ValueTask.FromResult(ZLinkSpotCreateResponse.Accept());
}

If creation is rejected, that reservation is never published as a Ready Spot. The caller receives a typed failure as the completion result.

8. What the ClientServer Channel Is For

TicTacToe's tictactoe.api ClientServer channel is used when a Play session requests user authentication from the API server. It isn't used for Game Spot creation.

// API process: handles the authentication request.
options.AddClientServerChannel(SampleChannels.Api)
    .Server()
    .Listen()
    .AddRequestHandler<
        AuthenticatePlayerHandler,
        AuthenticatePlayerReq,
        AuthenticatePlayerRes>();

// Play process: sends the authentication request.
options.AddClientServerChannel(SampleChannels.Api).Client();

Object creation and a ClientServer call are different features. No dedicated room-creation channel or CreateGameHandler is added.

9. Build and Run

# Build the sample solution first.
dotnet build framework/languages/dotnet/samples/TicTacToe/TicTacToe.sln

# Prepares Redis and 4 processes, and verifies the whole scenario.
framework/languages/dotnet/samples/TicTacToe/run_sample.sh

The runner runs 2 APIs and 2 Plays. After creating a Game Spot, it verifies that participants connected to different Play endpoints join the same room, then checks game messaging and end-of-game cleanup.

10. What to Check When It Fails

Symptom What to check
No creation candidate Check whether the Play process registered an Object Server and the GameSpot stable type on the same MeshName.
Startup fails Check the Redis connection, MeshName, listen endpoint, and any duplicate-registration error.
Creation is rejected Check the initial settings OnCreateAsync received and the reject reason.
The client can't join the room Check whether the HTTP response's RoomId was passed to the Actor join request as-is.

The next chapters each explain the role of the channel, Spot, Actor, Stream, and Location Store used here.