1. Overview¶
Guide Home | Next: 2. Getting Started
View in another language — C#/.NET · C++ · Java · Kotlin · Node/TypeScript
The documents that own this chapter's contract — owned by the Framework Overview and the per-language public contract index.
This document is the entry point of the
.NETguide. The guide explains the concepts and usage of ZLink Framework directly so anASP.NET Coredeveloper can read it and start writing code right away. The language-neutral formal definition of a concept is owned by the common spec overview, and the formal contract of the.NETpublic API is owned by the spec/interfaces index document. If the two disagree, the spec wins.
1. One-Line Definition¶
ZLink Framework is a real-time messaging framework that integrates with the major
framework you already use. The way Spring MVC sits on Spring as its web layer, ZLink
Framework sits on ASP.NET Core as a real-time messaging layer. It's not a switch to
a separate runtime or a dedicated server — it drops directly into the DI, hosted service,
configuration, and logging model you're already using.
This layer provides inter-server calls, pub/sub, and real-time state units. Inter-server
calls and pub/sub find their target purely by a logical channel name, with no separate
gateway or dedicated load balancer. The real-time state units are SPOT (room · stage ·
zone), an actor (a stateful object representing one connection/user), and STREAM (an
external client connection) — if these terms are unfamiliar, see the concept walkthrough in
03-concepts first. A developer writes a handler, client, and filter
with the same feel as using HTTP/gRPC, and the framework handles connection, location lookup,
routing, reconnect, and correlation.
ZLink is a framework used under the same contract across several languages. The same layer sits identically on Spring (Java/Kotlin) and NestJS (Node) too, and because the call contract is a language-neutral wire protocol (ZMP) + codec + logical channel/packet, services implemented in different languages call each other over the same channel (e.g., a room server in C++, an API server in .NET/Java). This guide is
.NET-based and treats the.NETimplementation as the reference implementation. The detailed cross-language model is covered by 17-alternative §2.1.
2. Situations Where You Need It¶
2.1 Building a Real-Time Game Server¶
What makes it hard. Game servers have no standardized framework like the web's
ASP.NET Core/Spring. This isn't an accident — there's a reason.
- The network topology each genre needs is different. Any web service is shaped the same way — "client request → server response" — which is why a framework could standardize around it. Games aren't. A board game needs room-based matching and turn progression, an MORPG needs a split between room/stage servers and matching/lobby, an MMORPG needs a zone/field server mesh and large-scale broadcast, an FPS needs a low-latency tick loop for a small session. Genre decides the topology, so there's no one fixed shape, and every team re-builds its own topology on top of raw sockets.
- State stays in memory. The web can put state in a DB and scale out statelessly, but a game keeps room/participant state in-memory for fast processing and runs its logic across multiple threads. That's the moment locks, contention, deadlocks, and the synchronization question "which thread is touching this room" seep into business logic.
- The connection itself is something to manage. Users keep long-lived connections. You handle socket framing and session lifetime directly, have to reconnect a user to whichever server and room they were in, and have to keep connected users and in-progress game state alive during a deployment or scale-down.
So up to now there were two choices — build all of this yourself, or move to a separate runtime, a game server engine, and relearn how you write logic, configure, deploy, and operate, on the engine's terms.
How it's actually been built. Using the names common in the industry, these approaches fall into roughly four patterns. Boxes like login/auth, gateway, and DB cache show up repeatedly no matter which pattern you pick — but since there's no common framework backing them, a team picks its genre's pattern and rebuilds that structure from the socket up.
- ① Zone-sharding. The world is split into geographic regions, one server (node) owns each region, and when a character crosses a boundary the simulation hands off to the adjacent region's server. This is the representative scaling approach for an MMORPG handling a large open world. Sharding (replicating the whole world and splitting players across copies) and instancing (spinning up several independent copies of the same region) are also common world-distribution approaches used together to handle a large number of concurrent players.
- ② Lobby + room. Users are received in the lobby/matching stage and assigned to a room, which owns participant state until that match ends. A room is usually a logical unit, with several running together inside one process. Common in casual, mobile MO, and board games.
- ③ Session-based dedicated fleet. Once matching tickets accumulate, the fleet assigns one dedicated server process for that match, and the client connects directly to that server. The process is returned once the match ends. Unlike ②, one match = one process is the base unit. The standard configuration for session-based games like competitive FPS and battle royale.
- ④ Stateful actor. Entity state, like a player or guild, is kept as an actor in server memory, and the DB only serves as periodic storage. It reduces read-heavy load and removes the need for a separate caching layer, so it's commonly used for meta/social backends. The representative frameworks are Orleans and Akka. One conceptual difference — Akka's actor isn't one user, it's a general-purpose concurrency unit used anywhere, and ZLink splits this into Spot (an execution-isolation unit) and Actor (a domain entity). What's closer to Orleans's virtual actor/grain isn't ZLink's Actor — it's the Instance Spot this approach uses. The detailed comparison is covered in Chapter 17 §6.
What ZLink provides. A feature answers each difficulty, one by one.
| Difficulty | ZLink feature | Details |
|---|---|---|
| Building a genre's topology from raw sockets | Declare topology by combining channels — 1:N request/response, fan-out, a node-addressed route mesh, a room-scoped spot mesh, all composed in a few lines of registration; the location store keeps connections up automatically | §3 Architecture · 05·06·10 |
| Locks/contention on in-memory state | SPOT serial execution — every message for one room lines up on a single execution line and runs in order. Locks disappear from business logic | The code below · 06 |
| Implementing socket framing/session lifetime directly | STREAM — the framework owns connection lifetime, framing, and packet codec (TCP/TLS/WS/WSS) | 09 |
| Tracking a reconnected user's location | Actor binding — a new connection after reconnect picks up the same actor | 08 |
| Users dropped during deployment | Graceful drain — blocks new admission, hands off actors, finishes in-progress work, then shuts down. 0 lines of app code | 12 |
And the four patterns above all become combinations on the same declarative model. There's no need to rebuild from the socket for each one.
- ① Zone-sharding — set up a zone with
AddRouteMesh+ a node-addressed route mesh. A player crossing a boundary is handed off by cross-node actor relocation (07) instead. ZoneWorld is exactly this approach. - ② Lobby + room — entry/matching is the Entry Spot, and a room is a room spot created
with
GetOrCreate. Bingo is exactly this approach. - ③ Matchmaker + dedicated — matching is implemented as a channel handler (HTTP, etc.).
Instead of spinning up a new process per match, the client connects over STREAM to the
room spot that was
GetOrCreated as the matching result. TicTacToe is closest to this flow — matching request → room/connection info response → connect to the already-prepared room spot. - ④ Actor service — an Instance Spot is cold-activated by entity ID and serially processes the state of an entity that several users access at the same time, with no Redis distributed lock. Continued in the guild service example.
Where the "existing approaches" diagram above split into four, here's how each approach assembles with ZLink, in the same spots.
Green (bold border) is the SPOT-family primitive. This is exactly where it contrasts with the "existing approaches" diagram above — each approach used to need its own infrastructure (a dedicated fleet orchestrator, sticky routing, an actor cluster), but in ZLink, all four are implemented with the same RouteMesh/Spot/Instance Spot combination. Switching approaches means no new runtime to learn.
A Twitch-scale FPS's ultra-low-latency snapshot netcode uses unreliable transport that tolerates loss. STREAM currently provides TCP/TLS/WS/WSS as transport, and unreliable transport (QUIC datagram/WebTransport) is planned. Even for that kind of game, though, matching/lobby/meta/social are handled just fine today by these four approaches. Exactly where the line falls is covered in Chapter 17 §4.
How is this different from a game server engine or service? Alternatives to building everything yourself include engines and managed services. Comparing what each provides by area makes ZLink's place clear.
| Area provided | Representative product | Form provided |
|---|---|---|
| Connection/transport optimization — socket/session management, encryption/compression, TCP/UDP in parallel, splitting network I/O from logic threads | ProudNet | Dedicated server module + client SDK |
| Room/lobby/matching — creating/finding a room, lobby, match invites | Photon·SmartFoxServer | A room model on its own runtime |
| Hosting/fleet — dedicated server allocation, autoscaling, a matchmaking rules engine (FlexMatch) | AWS GameLift·Agones | A cloud-managed service |
| Social/meta features — friends, leaderboards, groups, chat | Nakama | A backend server product |
ZLink provides connections/sessions (STREAM), rooms/state units (SPOT), inter-server messaging (channel), participant state (actor), and zero-downtime termination (host relocation) among these — but not as a dedicated runtime or managed service, as a library layer on the major framework you already use.
- Hosting/fleet isn't ZLink's job. Whether K8s or GameLift, a ZLink server just runs on top of it — it doesn't compete with a hosting service, it composes with one.
- Matchmaking rules and social features are app logic, not product features. You write them directly with a channel handler and spot. There's less pre-built for you, but the ownership and freedom over the logic stay with the app.
Instead of rebuilding for each language, ZLink puts the hard runtime in a single native
Core (C API) and wraps it in per-language layers. Per-language bindings connect that
C API to each language's socket API, and on top a per-language ZLink Framework provides
surfaces like RouteMesh · SPOT · actor · STREAM. The reason for this thin 3-layer split is
multi-language support — implement the Core once and swap only the language surface, and
C++, .NET, the JVM, and Node share the same core. bindings and the Core are the framework's
internal implementation, not exposed on the public API, and application code doesn't change
even if they're replaced later — this backend boundary is explained separately by
internals/backend-dependency-policy.
As code. Declare one room, and write that room's progression logic.
// Registration — one room mesh and a room type
var node = options.AddRouteMesh("game.room");
node.Listen("tcp://0.0.0.0:9001");
// A mesh has at least 1 logical membership
node.Channel("game.room").Server();
node.Objects().Server().AddSpotFactory<BingoRoomSpot>("room", factory => factory.RecreateOnRelocation());
// Bingo room progression code — no concurrency exists inside this.
public sealed class MarkNumberHandler
: IZLinkSpotRequestHandler<BingoRoomSpot, MarkNumber, MarkResult>
{
public ValueTask<MarkResult> HandleAsync(
BingoRoomSpot room, MarkNumber request, CancellationToken ct)
{
// No lock
room.Board.Mark(request.Number);
room.LastActivity = DateTimeOffset.UtcNow;
return ValueTask.FromResult(new MarkResult(room.Board.HasBingo()));
}
}
Several players send requests at the same time and a timer runs in this room, yet there's no
lock, no Interlocked, no Redis distributed lock. That's because the framework lines up
every message for one room (requests, subscription events, timer ticks, actor packets) on
a single execution line and runs them in order. Here, "serial" isn't codec serialization
— it's serialization of execution order (06 §3).
Runnable reference samples: TicTacToe · Bingo · GameQuest
2.2 Concurrent Access to One Entity¶
Why it's hard. There are cases, like a guild, where several different users need to modify the same entity at the same time. Just like two users applying to join at the same time can exceed the roster cap, or two donations landing at once can lose one of them, several stateless API servers touching the same row at the same time creates a race condition.
- Concurrent modifications collide. If several API instances read-modify-write the same guild row at the same time, a lost update happens.
- You have to assemble your own serialization mechanism. A Redis distributed lock or DB row lock has to build a per-guild critical section.
- The lock itself is a new failure mode. Lock acquisition failure, timeout, deadlock, and a stale write after lock expiry all land on the app to handle.
What ZLink provides. Instead of assembling a lock, it turns that entity into a serial execution unit.
| What you used to assemble | ZLink feature | Details |
|---|---|---|
| A Redis distributed lock per guild id | Instance Spot — one spot, cold-activated by guild id, processes every request for that guild serially | 06 |
| Lock acquire/release/timeout handling | Serial execution — the lock concept disappears entirely; everything is always processed in spot queue order | 06 §3 |
| Inter-server calls/LB to find the guild spot | channel name + location store | 05·10 |
| Pre-provisioning a new guild | Cold-activated on the spot when the first request arrives — no separate preparation needed |
The existing approach — lock acquire/release makes a round trip on every request.
The ZLink approach — the lock disappears, and the guild id itself becomes the spot address the request will arrive at.
A request for the same guild always passes through the same GuildSpot's queue, so the second request is only processed once the first finishes — it's not that another request is blocked for as long as the lock is held; two requests simply can never touch the same state at the same time in the first place.
As code. Where lock acquire/release used to sit, one call remains.
// Applying to join a guild — request directly by guild id. No prior lock, no prior creation.
await spots.RequestToSpot(guildId, new JoinGuildReq(userId))
.InstanceSpot("guild")
.InMesh("social")
.Async<JoinGuildRes>(ct);
There's no runnable reference sample for this scenario yet — the code above applies the same
API surface as GameQuest's PlayerQuestSpot registration/call approach to a guild.
2.3 Adding Real-Time Features to an Existing Web Service¶
Why complexity goes up. Consider a food-delivery order app: placing and viewing an
order are ordinary HTTP requests and responses, but status updates such as "preparing → out
for delivery → arriving soon" need to be pushed in real time without requiring the user to
refresh the app. The standard shape of a large web service — Spring/ASP.NET Core + Redis
(cache) + Kafka (events) + LB/K8s — is optimized for stateless request/response. The
moment you add a real-time feature like this, those assumptions stop fitting one by one, and
complexity rises.
- The connection becomes state. An HTTP request can land on any instance, but a WebSocket connection is pinned to one specific instance. That's how you end up with a sticky LB that pins connections, and the app starts managing "which instance is this user connected to right now" in Redis.
- Real-time delivery between servers has to take a detour. Since connections are scattered across instances, server-to-server delivery routes through a broker (Redis pub/sub, or even Kafka when you don't actually need replay) — one more piece of infrastructure to operate.
- Order-sensitive units appear. For an order or a conversation, the order events are processed in is correctness itself. Since several instances could touch the same order at the same time, you serialize with a distributed lock.
One feature bolted on, and you've grown a whole assembly kit — a WebSocket server, sticky LB, broker detour, distributed lock — plus the operational burden of running it.
What ZLink provides. A feature answers each piece of the kit.
| What you used to assemble | ZLink feature | Details |
|---|---|---|
| A WebSocket server + sticky LB | STREAM — the app server receives client connections directly | 09 |
| A distributed lock for ordering | SPOT owner routing — the same order/conversation always executes serially in its own one Spot | 06 |
| Real-time delivery through a broker | channel/fanout — inter-server delivery and fan-out go through transport directly | 05 |
| Managing "who's connected where" | Actor binding + location store — the framework owns reconnect portability and location lookup | 08·10 |
Drawing the same food-delivery order app — HTTP order processing + real-time delivery-status pushes — both ways shows the difference right in the picture.
The existing approach — the components for the real-time feature (orange) add up to as much as the main body.
The ZLink approach — every orange piece disappears, leaving one location store that provides node/actor/spot location information.
Three pieces of infrastructure — the sticky LB, pub/sub broker, and distributed lock — disappear. An Instance Spot preserves ordering, Session servers (STREAM) handle real-time connections instead of shell servers, and direct runtime connections handle inter-server delivery. The location store is the only new infrastructure.
As code. Where the distributed lock and sticky routing used to sit, the following code remains.
// Inside an HTTP handler — route an order event to that order's workflow Spot.
// The first request cold-activates the spot keyed on OrderId, and later requests arrive
// at the same already-created spot, always processed serially in one place (no distributed lock).
// request is already a StartOrderWorkflowReq body.
await spots.RequestToSpot(request.OrderId, request)
.InstanceSpot("order-workflow")
.InMesh("commerce")
.Async<StartOrderWorkflowRes>(ct);
// Inside an actor handler — push to a client that's still tied to the same actor after reconnect (no sticky LB).
await actor.Context.BoundSession.Send(new OrderStatusChanged(orderId, status)).Async(ct);
Runnable reference samples: SupportChat · DeliveryDispatch
2.4 Simplifying Event-Driven Business Processing¶
Where ZLink applies isn't limited to real-time features. Business processes like order handling, settlement, and inventory — where the same entity's events must be processed in order, without duplication — run into the same complexity problem even with zero real-time push on screen.
Why it gets complicated. The standard answer for this kind of work is a log-based pipeline like Kafka (an event-sourcing setup is usually built on top of this too). But what the log actually solves is "gather the same key in one place, in order," and a whole train of pieces follows just to get that one thing.
- Order is tied to a partition. To process the same order's events in order, you have to gather them by key partition, consumer count is tied to partition count, and consumer group rebalance and offset management follow as operational items.
- Consumers are stateless, so state means a DB round trip every time. Processing one event means reading, modifying, and writing current state in the DB every time. Adding a cache to cut repeated reads brings an invalidation problem along with it.
- At-least-once delivery pushes idempotency onto the app. Redelivery, rebalance, and reprocessing can bring the same event twice, so without a version check or a dedupe policy, it gets applied twice.
- You build a separate read model to query the processing result, and once the pipeline falls behind, lag monitoring and a resync job stay as leftover work.
Keeping state next to the consumer with a stateful stream processor (Kafka Streams/Flink) cuts the DB round trips, but partition design, state-store recovery, and rebalance remain your operational responsibility — the detailed comparison is covered by GameQuest common scenario §3.
Drawing the same business process — an order workflow — both ways shows the difference in pieces right in the picture.
The existing approach — the pipeline pieces for ordered processing (orange) add up to as much as the main body.
The ZLink approach — this doesn't replace Kafka. On the order-processing path, the pipeline pieces (orange) disappear, and Kafka stays in its natural role (gray) — propagating confirmed facts to independent systems and preserving events that need replay, as a durable log.
The key thing across the two pictures is that Kafka's color changes. Kafka (orange), which
used to own ordering inside the processing path, moves outside the processing path
and only handles propagation/preservation (gray). And with that, the pieces assembled just
for ordering — the order-processing consumer group (offset/rebalance/dedupe), the cache, the
read model for queries, the resync job — disappear. Since the same OrderId is always
processed serially by the same owner, there's no longer a need to assemble the ordering and
duplicate-prevention a pipeline used to provide.
The inter-server-call LB disappears too. Order processing calls other services like
inventory and payment synchronously, and the existing approach has to find and distribute to
the peer on every one of those paths via a K8s Service or service discovery (you can't
hardcode the address in code). In ZLink, you call by channel name, like "inventory",
and the location store tells you the currently available peer, so there's no separate LB
layer needed for inter-server calls — that's why the orange "LB for inter-server calls" is
gone in the after picture.
What stays, stays. Client HTTP ingress is still stateless, so an L7 LB/Ingress distributes to API servers as usual (gray), and order state is still stored in the DB. Unlike gRPC, this HTTP ingress path also doesn't additionally require an L7 distribution device (the reason is covered in Chapter 17 §5.1).
What ZLink provides. Solving "gather the same key in one place, in order" with owner routing instead of a log means most of the pieces above simply never need to be assembled.
| What you used to assemble | ZLink feature | Details |
|---|---|---|
| Key partition + consumer group | SPOT owner routing — the same OrderId always executes serially on the same Spot. Whichever API instance receives it, it's routed to the same owner |
06 |
| DB load-modify-store per event | The owner spot's hot state — state lives in the owner's memory, and the app decides when to persist based on business rules | 06 |
| Version check/distributed lock against redelivery | Serial execution — no concurrent writer for the same unit, so there's no lock/version contention on the normal path | 06 §3 |
| LB/service discovery for inter-server calls | channel name + location store — call by the name "inventory" and it sends directly to a currently available peer |
05·10 |
| Operating offset/lag/resync jobs | With no consumption pipeline, that operational item doesn't exist at all |
This doesn't replace your existing stack. Kafka stays exactly where it is, as a durable event stream, and Redis stays as cache/persistence support. What ZLink cuts is the complexity of connection, routing, and state management you used to assemble by hand in between.
The boundary stays where it is. Where a durable log is genuinely needed — event replay, long-term retention, broad fan-out to independent systems — Kafka is the right fit and stays exactly there (Chapter 17 §4). What ZLink cuts is the case where a log pipeline was assembled only for entity-scoped ordered processing. If order and consistency were the entire goal, owner routing achieves that goal directly, with no pipeline.
As code. Where the partition consumer used to sit, an owner Spot handler comes instead.
// Processing for the same OrderId always executes serially inside this Spot —
// no partition, no offset, no distributed lock, no idempotency retry policy to assemble.
public sealed class StartOrderWorkflowHandler :
IZLinkSpotRequestHandler<OrderWorkflowSpot, StartOrderWorkflowReq, StartOrderWorkflowRes>
{
public ValueTask<StartOrderWorkflowRes> HandleAsync(
OrderWorkflowSpot spot, StartOrderWorkflowReq request, CancellationToken ct)
// Accesses spot state without a lock
=> spot.StartOrderWorkflowAsync(request, ct);
}
Runnable reference sample: ShoppingMall — the reference sample for this exact situation, built with no real-time push at all, just an HTTP API + order workflow. It verifies order state transitions, compensation flow, duplicate prevention, and projection rebuild, all on top of owner routing.
The three situations differ only in entry point — the surface you use is the same. Products exist that provide one feature each — gRPC for RPC, Orleans for actors, a game engine for connections — but ZLink's niche is the combination that bundles major-framework integration + serial-execution state units + auto-connect topology into one.
3. Surface and Structure¶
3.1 The Call Unit — MeshName and ChannelName¶
An inter-server call in ZLink Framework picks its target by MeshName and
ChannelName. In the application, you use it like "send a request over the orders
channel in the services mesh." Which node handles that channel is decided by the framework,
which checks the membership registered in the location store.
The framework handles what you'd otherwise have written by hand to build one server.
| What you used to build yourself | How the framework handles it |
|---|---|
| Opening an endpoint, managing peer connections | Declare a MeshNode and STREAM node, and the hosted service connects them |
| Message serialization/deserialization | Codec registration and the handler contract exchange DTOs directly |
| Request routing/dispatch | Registering a typed handler on a ChannelName delivers the message to the right handler |
| Repeating common processing like logging/validation/authorization | An HTTP route uses middleware; a ZLink handler separates this into IZLinkHandlerFilter |
| Protecting state under concurrent requests | SPOT's serial execution manages state with no lock |
| Creating services, managing dependencies | ASP.NET Core DI creates the handler, client, and filter |
| Managing server addresses, deciding connections | Tracks the currently active endpoint through the location store |
| Configuration, logging, monitoring | Integrated with ASP.NET Core configuration/logging/hosted service |
3.2 How It Feels Compared with the Existing Approach¶
The difference in the amount of code needed to wire up the same "inter-server request/response."
Directly with raw bindings (conceptual):
// Location-store lookup, connecting the endpoint, reconnect management,
// correlation id matching, serialization, receive loop ... dozens of lines of connection/setup code
ZLink Framework:
// Server: one handler
public sealed class GetPriceHandler
: IZLinkRequestHandler<PriceRequest, PriceReply>
{
public ValueTask<PriceReply> HandleAsync(
PriceRequest request, IZLinkMessageContext context, CancellationToken ct)
// 187.42m is a fixed demo value (a real lookup result in practice)
=> ValueTask.FromResult(new PriceReply(request.Symbol, 187.42m));
}
// Registration — declares the MeshNode endpoint and the price membership's handler together.
builder.Services.AddZLinkFramework(options =>
{
// Scopes the communication range by MeshName.
options.AddRouteMesh("services")
// Opens this MeshNode's endpoint.
.Listen("tcp://0.0.0.0:7301")
.SetRoutingId(RoutingId.From("price-1"))
// Registers the price-handling membership.
.Channel("price")
.Server()
// Registers this channel's request handler.
.AddRequestHandler<GetPriceHandler>();
});
// Client: inject IZLinkRouteClient and call by ChannelName.
var reply = await client
.RequestToChannel(
// The ChannelName to look up process-locally
"price",
new PriceRequest("AAPL"))
// Sends, then waits for the reply asynchronously.
.Async<PriceReply>(ct);
The connection/setup code disappears, leaving a handler and a few lines of channel registration.
3.3 Layering and Registration Points¶
Put the host framework you already use (ASP.NET Core · Spring Boot · NestJS · C++ host) at
the bottom and register the ZLink Framework into it with AddZLinkFramework — the opposite
of bringing in a new engine and moving to a separate ecosystem; all of this runs inside the
framework you already use. On top of that, the only code you write is the business logic
(Spot · Actor · handler), and the Framework exposes its own functionality through the
DI · hosted service · handler · attribute model.
The point where the application meets this stack is one registration spot. This is where you declare the MeshNode, fanout, and STREAM node.
builder.Services.AddZLinkFramework(options =>
{
// Provides node/actor/spot location info — connections between nodes are automatic on top of this
options.AddLocationStore(new ZLinkRedisLocationStore(...));
// MeshNode for inter-server request/send
options.AddRouteMesh("services")
.Listen("tcp://0.0.0.0:7301")
.SetRoutingId(RoutingId.From("service-a"))
// The logical membership to handle
.Channel("orders").Server();
options.AddFanoutChannel("events")
// classic event fan-out
.EnablePublisher("tcp://0.0.0.0:7302");
// SPOT/actor are also owned by a MeshNode
options.AddRouteMesh("game.room")
.Listen("tcp://0.0.0.0:7304")
.SetRoutingId(RoutingId.From("room-a"))
.Channel("game.room").Server();
options.AddStreamNode("gateway")
// The external client endpoint
.Bind("tcp://0.0.0.0:7400");
});
Topologies you used to assemble separately with gRPC+LB, a broker, and a WebSocket server all collapse down to one declarative model. Once the location store is registered, connections auto-connect and auto-clean-up as servers scale up or down — nothing to edit in a config file, no LB to reconfigure. (05·06·09·10)
What you declare, and where, comes down to three spots.
| Surface | Role | Chapter that covers it |
|---|---|---|
builder.Services.AddZLinkFramework(...) |
Declare channel/SPOT/STREAM | Chapter 5~Chapter 9 |
options.AddRouteMesh(...) / AddFanoutChannel(...) |
Declare RouteMesh/fanout | Chapter 5 |
IZLink*Runtime status |
Status observation and diagnostics | Chapter 11 |
Every option settable at each surface, with its default, is collected in 16-options.
4. The Four Integration Axes, Summarized¶
| Axis | What the user sees | Guide chapter |
|---|---|---|
| channel messaging | IZLinkRequestHandler, IZLinkSendHandler, IZLinkRouteClient, IZLinkHandlerFilter |
05-channel-messaging |
| fanout | AddFanoutChannel, IZLinkFanoutHandler |
05-channel-messaging |
| SPOT | Typed spot factory, Spot context outbound, timer | 06-spot |
| actor / session | Actor factory, Entry Spot, IZLinkBoundSession, session actor dispatch |
07-actor-spot · 08-actor-session |
| STREAM | Framework session packet, Stream Connector | 09-stream |
| Infrastructure | Location-based auto-connect/operational queries, runtime monitoring | 10-location, 11-monitoring |
| Operations | Runtime metrics (one AddMeter line), graceful drain, readiness probe |
12-operations |
5. The Overall Topology¶
An example showing how each feature fits together. Each feature's own chapter zooms into part of this map.
- Entry server — receives an external request over ASP.NET Core HTTP and delegates to the domain server.
- Domain server — MeshNode channel membership + SPOT (state unit) + session relay + stream node.
- Location store — manages server address information. The dotted lines are connections that find an endpoint through a store lookup.
- Client app — sends requests over HTTP, and receives real-time state over stream.
6. Who This Guide Is for, and Its Scope¶
This guide focuses on when to reach for channel, handler, SPOT, STREAM, and the location store, rather than the runtime's internal structure.
The main readers are:
- A backend API developer: someone sending a request to another internal service from
inside an HTTP endpoint, or looking to replace an existing gRPC call with a logical
channel name-based request/response. - A microservices operations developer: someone who wants server instances to scale up and down while auto-connecting through the current server list the location store manages, without hardcoding addresses in code.
- A real-time service developer: someone who wants to bundle a stateful unit — a game room, stage, zone, or an order workflow — into a SPOT, and process packets arriving at the same state in one execution flow.
- A gateway/connector developer: someone who wants to receive an external client over STREAM — TCP, TLS, WebSocket — and hand internal processing off to a channel or an actor.
- A tech lead or reviewer: someone judging whether a problem is worth adopting ZLink for, and checking which responsibilities ZLink should own versus which should stay with the DB, broker, or domain service.
To see ZLink's use concretely, as a business flow, look at the common samples. See the real-time game server shape in TicTacToe and Bingo. ShoppingMall, DeliveryDispatch, GameQuest, and SupportChat are end-to-end samples that go all the way to the business domain — order workflow, assignment/status tracking, game progress, support/chat.
What this layer doesn't do is also clear. ZLink Framework isn't a layer that exposes a transport implementation to the application
code. An application developer uses the public feature set through the DI, hosted service, handler, and location store model. Anyone reviewing the formal public API contract should also read the spec/interfaces index; anyone changing the runtime's internal structure should also read internals/.
7. Naming Convention¶
The guide uses the following notation consistently throughout.
- Every public type the framework adapter exposes (interface, record, enum,
attribute, exception, DI extension method) uses the
ZLinkprefix (capitalL). e.g.IZLinkRouteClient,IZLinkMessageContext,[ZLinkRequest],AddZLinkFramework,ZLinkFrameworkException. - Except, types in the client-side Stream Connector package
(
Zlink.Stream.Connector) use theZlinkprefix (lowercasel). e.g.IZlinkStreamConnector,ZlinkStreamConnectorOptions,ZlinkStreamMessage. This is because the connector is an independent client library with no dependency on the server framework package. - The underlying zlink core C API is
zlink_*snake_case. - NuGet package ids use
ZlinkorZlink.*. Namespace and assembly names retain their established values and are independent from the package id.
In short: server framework =
ZLink, client connector =Zlink. If you see both notations in the same code, it's not a typo — it's this rule.
8. Guide Reading Order¶
- 02-getting-started — From the package to your first working check
- 03-concepts — Core concepts (channel, roles, DI)
- 05-channel-messaging — request/send/pub-sub in detail
- 06-spot — room/stage/zone, timer, routed Spot calls
- 07-actor-spot — Actor lifecycle, Spot hosting/callbacks
- 08-actor-session — session↔actor binding/dispatch, client push
- 09-stream — The external-client (STREAM) server + Stream Connector
- 10-location — Location-store-based auto-connect and operational queries
- 11-monitoring — Status observation and diagnostics
- 13-interface-catalog — Every contract interface as code (ContractTests verification)
- 14-samples — Checking it against runnable samples
- 16-options — The option list and defaults, what changes when
- 17-alternative — Where ZLink fits (where it's used, warning signs, the technology-choice boundary)
- Common samples — Representative business scenarios and verification criteria
- spec/interfaces index — The formal contract (interface catalog)