Skip to content

.NET Quickstart — from Install to a First Request

What you get from this chapter

You can install the packages and run a minimal project where two processes call each other.

The project lives at framework/languages/dotnet/quickstart/. The code blocks below are read from those files when the site is built. Without a location store, two processes name each other's endpoint directly and exchange one request/reply.

0. Clone the examples repository

This chapter's project is quickstart/ in the zlink-dotnet-examples repository; tutorial/, the program read by the feature guides, and samples/ live beside it.

git clone https://github.com/zlink-systems/zlink-dotnet-examples.git
cd zlink-dotnet-examples/quickstart

main is the latest release plus the fixes merged since, with package versions pinned to that release. An older release is the tag vA.B.C (git checkout vA.B.C). Send issues and PRs to zlink-systems/zlink.

1. Installation

  • .NET SDK 8.0 or later (net8.0)
  • Access to nuget.org

Get it from NuGet. The minimal combination needed to build one server is the following.

# The contract and runtime. It includes the core messaging engine (Zlink) transitively
dotnet add package Zlink.Framework
# DI/hosted service registration (AddZLinkFramework)
dotnet add package Zlink.Framework.AspNetCore

Do not list Zlink (the binding) yourself. Zlink.Framework declares the version it depends on.

Directory.Packages.props
<Project>

  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <!-- 계약과 runtime. Zlink(binding)는 여기서 고정하지 않고
         Zlink.Framework의 전이 의존에 맡긴다. -->
    <PackageVersion Include="Zlink.Framework" Version="0.30.0" />
    <!-- DI·hosted service 등록(AddZLinkFramework). -->
    <PackageVersion Include="Zlink.Framework.AspNetCore" Version="0.30.0" />
  </ItemGroup>

</Project>
nuget.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

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 (Location)
Zlink.Framework.Codecs.Protobuf · .MessagePack To use instead of the default JSON codec (Handlers and Message Processing)
Zlink.Stream.Connector When building an external client (a game client, mobile) (STREAM)
Zlink.HttpClient When the server calls out over HTTP (HTTP Client guide)

The license differs by layer — core/binding is MPL-2.0, framework is FSL-1.1-ALv2, and Zlink.HttpClient is Apache-2.0. There is no cost to building and selling a service.

2. Shared contract

Shared/Contracts.cs
namespace QuickStart.Shared;

// Request/reply contract shared by the server and client processes.
public sealed record Hello(string Name);

public sealed record Greeting(string Text);

3. The handling side

A handler implementing IZLinkRequestHandler<,> is registered directly on Channel(...).Server().AddRequestHandler<...>(). That registration is what decides which channel exposes it.

Server/Program.cs
using QuickStart.Shared;
using Zlink.Framework.AspNetCore;
using Zlink.Framework.Contracts.Handlers;

var builder = WebApplication.CreateBuilder(args);

// This process does not expose HTTP; the port only needs to differ from the
// client process's port when both run on the same host.
builder.WebHost.UseUrls("http://127.0.0.1:5081");

builder.Services.AddZLinkFramework(options =>
{
    // Names the mesh and opens this process's endpoint for peers to connect to.
    var mesh = options.AddRouteMesh("services").Listen("tcp://127.0.0.1:7101");
    // This process handles the "greeting" channel. An IZLinkRequestHandler<,>
    // class registers on the channel builder; AddHandlersFromAssemblyOf wires
    // only attribute-declared handlers.
    mesh.Channel("greeting").Server().AddRequestHandler<HelloHandler, Hello, Greeting>();
});

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

// Handles one request on the "greeting" channel.
public sealed class HelloHandler : IZLinkRequestHandler<Hello, Greeting>
{
    public ValueTask<Greeting> HandleAsync(
        Hello request,
        IZLinkMessageContext context,
        CancellationToken cancellationToken
    ) => ValueTask.FromResult(new Greeting($"hello, {request.Name}"));
}
Server/Server.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <IsPackable>false</IsPackable>
    <RootNamespace>QuickStart.Server</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Zlink.Framework" />
    <PackageReference Include="Zlink.Framework.AspNetCore" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="../Shared/Shared.csproj" />
  </ItemGroup>

</Project>

4. The calling side

Both processes run on the same host, so each names its own HTTP port. Without that they collide on Kestrel's default port.

Client/Program.cs
using QuickStart.Shared;
using Zlink.Framework.AspNetCore;
using Zlink.Framework.Contracts.Channels;

var builder = WebApplication.CreateBuilder(args);

// http://127.0.0.1:5080/hello/{name} is this process's own HTTP surface.
builder.WebHost.UseUrls("http://127.0.0.1:5080");

builder.Services.AddZLinkFramework(options =>
{
    // This process also needs its own endpoint.
    var mesh = options.AddRouteMesh("services").Listen("tcp://127.0.0.1:7102");
    // This side only calls; it does not handle "greeting".
    mesh.Channel("greeting").Client();
    // Manual connection — the server's endpoint is given 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 a single ChannelName; which node handles it is not specified.
        var reply = await route
            .RequestToChannel("greeting", new Hello(name))
            .Async<Greeting>(cancellationToken);

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

await app.RunAsync();

5. Run

cd zlink-dotnet-examples/quickstart
dotnet build

# Two terminals. Start the server first.
dotnet run --project Server/Server.csproj
dotnet run --project Client/Client.csproj

curl http://127.0.0.1:5080/hello/world

The response is "hello, world" with status 200.

6. What to check when the first run fails

Symptom What to check
A package is not found Check that the package names in section 1 were copied exactly. The binding version is not pinned separately
Startup fails Check that both processes name the same mesh, and that the listen endpoint does not collide with another process
A call ends with no target Check that the receiving side registered that channel name in the server role, and that the two processes are connected as peers
No answer arrives Check that the caller used request. A send receives no answer

7. What to carry over

File Content
Directory.Packages.props The two PackageVersion entries. The binding stays transitive
Shared/Contracts.cs record-based message definitions
Server/Program.cs AddRouteMesh → Listen → Channel(...).Server().AddRequestHandler<...>()
Client/Program.cs Channel(...).Client(), PeerConnections.Connect(...), RequestToChannel(...).Async<T>()

These two processes connect by writing each other's endpoint directly. Keeping the calling code unchanged while servers are added or restarted at another address needs automatic connection, and that is covered by Location.