.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.
<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>
<?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¶
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.
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}"));
}
<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.
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>() |
8. What to read next¶
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.
- To go over the concepts first — Core Concepts
- The path that calls by name — Channel Messaging
- State objects called by id — Spot · Actor
- To see a complete business flow — Picking a Sample