Skip to content

C# Client Library

The Virtufin API Client library (Virtufin.Api.Client) provides a high-level C# interface for interacting with the Virtufin API gateway.

Installation

From Gitea NuGet Registry

Add the Virtufin package source with a personal access token (scope: read:packages):

dotnet nuget add source https://nuget.haenerconsulting.com/api/packages/virtufin/nuget/index.json \
  --name Virtufin --username <your-gitea-username> --password <your-gitea-token>

dotnet add package Virtufin.Api.Client

Or configure via NuGet.Config:

<configuration>
  <packageSources>
    <add key="Virtufin" value="https://nuget.haenerconsulting.com/api/packages/virtufin/nuget/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <Virtufin>
      <add key="Username" value="<your-gitea-username>" />
      <add key="ClearTextPassword" value="<your-gitea-token>" />
    </Virtufin>
  </packageSourceCredentials>
</configuration>

From Local Source

<ProjectReference Include="path/to/Virtufin.Api.Client/Virtufin.Api.Client.csproj" />

Overview

The client library provides three main classes:

Class Description
ApiClient Main entry point with async-only operations
GatewayClient Low-level gateway operations (async-only)
ServiceClient Service-specific method invocation (async-only)

Note: This library is async-only. For synchronous usage patterns, wrap calls in Task.Run() or use a different approach.

Quick Start

using Virtufin.Api.Client;

// Create and dispose when done
        using var client = new ApiClient("localhost", 5002);

        // Discover services
        Console.WriteLine("Available Services:");
        var services = await client.ListServicesAsync();
        foreach (var service in services)
        {
            Console.WriteLine($"  - {service}");

            // List methods for each service
            var methods = await client.ListMethodsAsync(service);
            foreach (var method in methods.Take(3)) // First 3 methods
            {
                var streaming = method["isServerStreaming"] == "True" ? " (stream)" : "";
                Console.WriteLine($"    {method["name"]}{streaming}");
            }
            if (methods.Count > 3)
                Console.WriteLine($"    ... and {methods.Count - 3} more");
        }

        Console.WriteLine();

        // Invoke a method
        Console.WriteLine("Invoking workmanager.ListWorkers:");
        try
        {
            var response = await client.InvokeAsync("workmanager", "ListWorkers");
            Console.WriteLine($"Response: {System.Text.Json.JsonSerializer.Serialize(response)}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Dynamic Dispatch

The C# client uses the dynamic keyword to allow client.Gateway.<service>.<method>() syntax. Under the hood, GatewayClient and ServiceClient both extend System.Dynamic.DynamicObject and override TryGetMember:

  • client.Gateway.<service> triggers GatewayClient.TryGetMember, which returns a cached ServiceClient for the named service.
  • <service>.<method> triggers ServiceClient.TryGetMember, which returns a Func<Dictionary<string, object?>, Task<Dictionary<string, object?>>> that invokes the named method via the Gateway's InvokeJson RPC.

The dictionary argument is serialized to JSON and sent to the Gateway, which unmarshals it to the typed proto request, runs the backend RPC, and marshals the response back to JSON.

Dynamic Keyword Semantics

Because the accessor returns a dynamic, you can either:

// Option 1: assign to a 'dynamic' variable, then call methods on it.
dynamic workmanager = client.Gateway.workmanager;
var workers = await workmanager.ListWorkers();

// Option 2: chain the access in a single expression.
var workers = await client.Gateway.workmanager.ListWorkers();

The result of <service>.<method>(...) is Task<Dictionary<string, object?>>, so it must be awaited. The Dictionary<string, object?> is the JSON-unmarshaled response; field access uses string keys (e.g. w["id"]).

WorkManager

using var client = new ApiClient("localhost", 5002);
dynamic workmanager = client.Gateway.workmanager;

// ListWorkers — no request fields; returns {"workers": [...]}
var workers = await workmanager.ListWorkers();
foreach (var w in workers["workers"])
    Console.WriteLine($"  {w["id"]}: {w["status"]} ({w["language"]})");

// CreateWorker — CodeSource oneof (url | content), mime_type, topic, group
var createResult = await workmanager.CreateWorker(new Dictionary<string, object?>
{
    ["code_source"] = new Dictionary<string, object?> { ["url"] = "https://example.com/worker.py" },
    ["mime_type"] = "text/x-python",
    ["topic"] = "worker-commands",
});
var workerId = (string)createResult["id"];

// StartWorker — id field
await workmanager.StartWorker(new Dictionary<string, object?> { ["id"] = workerId });

WebSocketManager

dynamic wsm = client.Gateway.websocketmanager;

// List — no request fields; returns {"connections": [...]}
var connections = await wsm.List();
foreach (var c in connections["connections"])
    Console.WriteLine($"  {c["id"]}: {c["url"]} ({c["status"]})");

// Connect — url, auto_reconnect
var conn = await wsm.Connect(new Dictionary<string, object?>
{
    ["url"] = "wss://stream.example.com/feed",
    ["auto_reconnect"] = true,
});
var connectionId = (string)conn["id"];

// Disconnect — id field
await wsm.Disconnect(new Dictionary<string, object?> { ["id"] = connectionId });

See also: proto-to-client-mapping spec §Layer 3a (C# wrapper) and §Layer 4 (Dynamic dispatch).


Pub/Sub Operations

The ApiClient provides pub/sub messaging via the Pubsub gRPC service.

Methods

Task<PublishResponse> PublishEventAsync(string topic, byte[] data, Dictionary<string, string>? metadata = null, CancellationToken cancellationToken = default)
Task<PubsubSubscribeResponse> PublishWithResultAsync(string topic, byte[] data, string replyTopic, TimeSpan? timeout = null, Dictionary<string, string>? metadata = null, string? correlationId = null, CancellationToken cancellationToken = default)
AsyncServerStreamingCall<PubsubSubscribeResponse> SubscribeToTopic(string topic, CancellationToken cancellationToken = default)
Task<UnsubscribeResponse> UnsubscribeFromTopicAsync(string subscriptionId, CancellationToken cancellationToken = default)

PublishWithResultAsync implements the request-reply pattern over pub/sub using correlation IDs. Parameters:

  • topic — Topic to publish the request to
  • data — Request data as byte[]
  • replyTopic — Topic to listen for the correlated response on
  • timeout — Maximum wait time (default: 30s)
  • metadata — Optional additional metadata
  • correlationId — Optional correlation ID (auto-generated if null)

Raises: TimeoutException if no response arrives within the timeout.

Example: Publish

var response = await client.PublishEventAsync("my-topic",
    System.Text.Encoding.UTF8.GetBytes("{\"key\": \"value\"}"));
Console.WriteLine($"Published: {response.Status.Success}");

Example: Request-Reply

var response = await client.PublishWithResultAsync(
    topic: "worker-commands",
    data: System.Text.Encoding.UTF8.GetBytes("{\"command\": \"status\"}"),
    replyTopic: "worker-responses",
    timeout: TimeSpan.FromSeconds(10)
);
Console.WriteLine($"Response: {Encoding.UTF8.GetString(response.Data.ToByteArray())}");

Example: Subscribe

using var sub = client.SubscribeToTopic("my-topic");
await foreach (var evt in sub.ResponseStream.ReadAllAsync())
{
    Console.WriteLine($"Event: {evt.Topic} - {Encoding.UTF8.GetString(evt.Data.ToByteArray())}");
}

Protobuf Dependencies

The client library requires the generated protobuf classes. Ensure your project references:

<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Grpc.Tools" />

The proto files (gateway.proto, config.proto) must be compiled into: - Virtufin.Api.Protos.Gateway - Virtufin.Api.Protos.Config

SDK Reference