Skip to content

.NET SDK

The official C# client for IVK Match. It wraps the gRPC surface, ships the generated message types, and provides helpers for packing engine payloads.

Terminal window
dotnet add package Invokation.Match.Sdk
using Invokation.Match.Sdk;
using var sdk = MatchSdk.CreateBuilder()
.WithBaseUrl("https://match.ivk.dev")
.Build();

The client is disposable and intended to be long-lived. Create one per process and share it; it holds a gRPC channel, and constructing one per request costs connections and latency.

Builder optionRequiredPurpose
WithBaseUrl(string)YesThe endpoint to connect to
WithRetryConfig(RetryConfig)NoRetry policy for transient failures
WithHttpClient(HttpClient)NoSupply a custom HttpClient, for credentials or custom transport behaviour
WithLogger(ILogger)NogRPC diagnostics

Wrap an HttpClient in a handler that adds the Authorization header to every call:

sealed class BearerTokenHandler(string token) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Authorization = new("Bearer", token);
return base.SendAsync(request, ct);
}
}
var httpClient = new HttpClient(new BearerTokenHandler(token)
{
InnerHandler = new HttpClientHandler(),
});
using var sdk = MatchSdk.CreateBuilder()
.WithBaseUrl("https://match.ivk.dev")
.WithHttpClient(httpClient)
.Build();

Load the token from a secret store. See Authentication for where credentials belong and how to rotate them.

using Google.Protobuf;
using Matchmaker.Core.V1;
using Matchmaker.Engines.Nemesis.V1;
var input = new EngineInput
{
Players =
{
new Player
{
PlayerId = "alice",
Mmr = 0.62,
Latencies = { ["eu-central"] = 22, ["eu-west"] = 41 },
},
},
Attributes =
{
["playlist"] = new Attribute { Values = { "TDM" } },
["platform"] = new Attribute { Values = { "pc" }, Accepts = { "pc", "xbox" } },
},
};
var ticket = new Ticket
{
Id = Guid.NewGuid().ToString(),
QueueId = "acme.quickplay",
RulesetIds = { "5v5" },
EngineInput = Nemesis.PackInput(input),
Metadata = ByteString.CopyFromUtf8(sessionId),
};
var ticketId = await sdk.CreateTicketAsync(ticket, ct);

Three details in this example are deliberate.

The caller generates the ID, and it must be a UUID. Because the caller controls the ID, retrying after a timeout is idempotent: the same ID cannot produce two pool entries.

It does not set CreatedAt. The server sets it and overwrites any value the client sends. It is the basis of queue position, so it has to be server-authoritative.

Metadata comes back verbatim. Nothing reads it, and it is included on every outcome about this ticket. Session and party IDs can be stored there instead of in a separate lookup table.

The return value confirms the ticket is durably queued. It is not a match. Matches arrive asynchronously, over the sink configured on the queue.

MethodPurpose
CreateTicketAsync(ticket, ct)Put a player or party into a queue
CancelTicketAsync(queueId, ticketId, ct)Remove a ticket from matchmaking
ReactivateTicketsAsync(queueId, ticketIds, ct)Return resolved tickets to the pool after a match that could not be honoured
CreateBackfillRequestAsync(backfillRequest, ct)Ask for players to fill a running match
CancelBackfillRequestsAsync(queueId, backfillIds, ct)Withdraw backfill requests
ClearQueuePoolAsync(queueId, ct)Empty a queue’s pool. Test tooling; it ejects every waiting player

Request and response semantics for each are in the Matchmaking API reference.

engine_input and engine_output are engine-defined protobuf messages carried as bytes. Every queue runs Nemesis, and the Nemesis helper packs and unpacks both:

var output = Nemesis.UnpackOutput(match.EngineOutput);
var preferredHost = output.Host.Preferred;
var teams = output.Teams;

The package contains helpers for other engines. They are being deprecated; use Nemesis.

The SDK does not consume outcomes. They are pushed to the sink configured on the queue rather than pulled through the client, so consuming them is a separate path in the game backend: an HTTP handler or a WebSocket consumer, not a client call.

See Outcome Delivery for the sinks, and Integration Flow for how to handle a match when it arrives.

Failures surface as RpcException carrying the gRPC status codes documented in Matchmaking API § Error semantics. Two need specific handling:

  • Unavailable: retry with backoff rather than surfacing it to the player. It is usually a brief restart window, and tickets already queued survive it.
  • FailedPrecondition on CancelTicketAsync: the player is very likely already in a match whose outcome is in flight. A failed cancel does not mean the player left the queue.