.NET implementation of Vertex — a lightweight, cross-language bidi messaging kernel.
Migrated out of Skywalker.Messaging.* with a new serializer abstraction, transport-forced Protobuf on gRPC, and proto-FullName topic alignment to vertex-go. Cross-language interop verified end-to-end via Vertex/compat/hello.
| NuGet package | Role |
|---|---|
Vertex.Serialization.Abstractions |
IMessageSerializer interface |
Vertex.Serialization.Protobuf |
ProtobufMessageSerializer (required by Vertex.Transport.Grpc) |
Vertex.Serialization.MessagePack |
MessagePackMessageSerializer (default for Vertex.Transport.NetMq) |
Vertex.Transport.Abstractions |
ITransport, peer / frame primitives, the 4-invariant contract |
Vertex.Transport.NetMq |
ZeroMQ transport; user-supplied serializer (MessagePack default) |
Vertex.Transport.Grpc |
gRPC transport (client + server); Protobuf enforced |
Vertex.Messaging.Abstractions |
IMessageBus, IRpcClient, IRpcHandler<,> |
Vertex.Messaging |
MessagingChannel |
Namespaces mirror the package names (namespace Vertex.Messaging { ... }, etc.).
Target framework: net8.0 initially.
dotnet add package Vertex.Messaging
dotnet add package Vertex.Transport.Grpc # cross-language scenarios
# or
dotnet add package Vertex.Transport.NetMq # intra-cluster scenariosPackages are published to this repo's GitHub Packages feed. Configure a nuget.config with a <packageSource> pointing at https://nuget.pkg.github.com/dengxuan/index.json and a PAT with read:packages.
Define business messages in .proto:
// protos/gaming.proto
syntax = "proto3";
package gaming.v1;
message CreateRoom { string room_name = 1; }
message RoomCreated { string room_id = 1; }Wire it up:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddVertexGrpcTransport("main", o =>
{
o.ServerAddress = new Uri("https://api.example.com");
});
builder.Services.AddVertexMessaging("main", reg =>
{
reg.RegisterEvent<gaming.v1.GameStateChanged>();
reg.RegisterRequest<gaming.v1.CreateRoom, gaming.v1.RoomCreated>();
});Use it:
public class RoomService(IRpcClient rpc)
{
public Task<gaming.v1.RoomCreated> CreateAsync(string name, CancellationToken ct) =>
rpc.InvokeAsync<gaming.v1.CreateRoom, gaming.v1.RoomCreated>(
new() { RoomName = name }, cancellationToken: ct).AsTask();
}[MessagePackObject]
public class CreateRoom { [Key(0)] public string RoomName { get; set; } = ""; }
builder.Services.AddVertexNetMqTransport("internal", o =>
{
o.BindEndpoint = "tcp://*:5555";
// o.Serializer defaults to MessagePackMessageSerializer;
// pass any IMessageSerializer to opt into Protobuf / JSON / etc.
});public class CreateRoomHandler
: IRpcHandler<gaming.v1.CreateRoom, gaming.v1.RoomCreated>
{
public ValueTask<gaming.v1.RoomCreated> HandleAsync(
gaming.v1.CreateRoom req, RpcContext ctx, CancellationToken ct)
{
var id = Guid.NewGuid().ToString("N");
return ValueTask.FromResult(new gaming.v1.RoomCreated { RoomId = id });
}
}
// in Startup:
builder.Services.AddScoped<
IRpcHandler<gaming.v1.CreateRoom, gaming.v1.RoomCreated>,
CreateRoomHandler>();dotnet restore Vertex.sln
dotnet test
dotnet packMinVer-based versioning: git tags of the form v<major>.<minor>.<patch> drive the NuGet version (see the Skywalker setup for precedent).
Vertex is transport-layer messaging, not a durable broker. The distinction matters:
IMessageBus.PublishAsync<T>(event) sends an EVENT envelope over the current stream. No ACK, no retry, no persistence. Lost when:
- network drops bytes mid-flight
- server crashes between receive and dispatch
- a subscriber handler throws (logged, but event not reprocessed)
- the gRPC stream resets for any reason
PublishAsync returns once frames are on the wire. It cannot tell you whether the subscriber ran.
Use for: real-time notifications, cache invalidations, telemetry. Don't use for: orders, payments, audit log — anything with a correctness requirement.
IRpcClient.InvokeAsync<TReq, TResp>(request) sends a REQUEST and awaits the matching RESPONSE. The caller learns about every failure path:
| Failure mode | What InvokeAsync does |
|---|---|
| Transport fails to send | throw (transport exception) |
| Server has no handler for this type | throw RpcRemoteException("No RPC handler registered…") |
| Server handler throws | throw RpcRemoteException(<message>) |
| Connection drops before response arrives | throw RpcPeerDisconnectedException |
| Response doesn't arrive within the timeout | throw RpcTimeoutException |
| CancellationToken cancelled | throw OperationCanceledException |
The app decides retry / idempotency / fallback — Vertex only guarantees you know whether it worked.
Use for: anything with a business correctness requirement.
| Your flow | Recommended |
|---|---|
| Real-time UI update | PublishAsync |
| Cache invalidation | PublishAsync |
| Payment confirmation | InvokeAsync or broker |
| Provision a resource | InvokeAsync |
| Audit log entry | InvokeAsync, or a broker/outbox for durability |
| Periodic heartbeat | PublishAsync |
Out of scope. Use Kafka / RabbitMQ / NATS JetStream. Vertex sits above the transport; it cannot add persistence on its own.
Before pointing real traffic at a Vertex-based client:
-
CancellationTokenwith deadline on every PublishAsync / InvokeAsync call — a stuck server should not block the caller indefinitely. - Logger wired:
ILoggerFactoryfrom the host is used automatically byMessagingChannel(via standard DI) — make sure your logging backend captures Warn-level lines (those are the ones that fire on dropped events / handler exceptions). -
InvokeAsynctimeout ≤ server handler's worst-case latency — callers that bail early while the handler still completes leave orphan work. - Dispose the host gracefully — the built-in
MessagingChannelStarterIHostedService drains the channel onIHost.StopAsync();Ctrl+Con a console app does this automatically, KubernetesSIGTERM→IHostApplicationLifetime.ApplicationStoppingalso works. - Use
InvokeAsync, notPublishAsync, for must-deliver flows — see the decision table above.
The authoritative wire format and transport contract live in the Vertex spec repo. Any wire-spec change lands there first, with a companion PR here.
Key documents:
- Wire format
- Transport contract (4 invariants)
- Cross-language interop test: Vertex / compat / hello
See CONTRIBUTING.md.
MIT — see LICENSE.