SiLA2.Client 10.2.2

dotnet add package SiLA2.Client --version 10.2.2
                    
NuGet\Install-Package SiLA2.Client -Version 10.2.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SiLA2.Client" Version="10.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SiLA2.Client" Version="10.2.2" />
                    
Directory.Packages.props
<PackageReference Include="SiLA2.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SiLA2.Client --version 10.2.2
                    
#r "nuget: SiLA2.Client, 10.2.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SiLA2.Client@10.2.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SiLA2.Client&version=10.2.2
                    
Install as a Cake Addin
#tool nuget:?package=SiLA2.Client&version=10.2.2
                    
Install as a Cake Tool

SiLA2.Client

Client-Side gRPC Communication and Server Discovery for SiLA2 Applications

NuGet Package SiLA2.Client on NuGet.org
Repository https://gitlab.com/SiLA2/sila_csharp
SiLA Standard https://sila-standard.com
License MIT

Overview

SiLA2.Client is a foundational module of the sila_csharp implementation that provides essential client-side utilities for connecting to and communicating with SiLA2 servers. It combines server discovery via mDNS, gRPC channel management, and dependency injection into a unified client configuration framework.

Key Capabilities

This module provides four core capabilities for building SiLA2 client applications:

  1. Server Discovery - Automatic detection of SiLA2 servers on the local network using mDNS/DNS-SD
  2. gRPC Channel Management - Creating and configuring secure gRPC channels with TLS/encryption
  3. Dependency Injection Setup - Simplified DI container configuration for client applications
  4. Configuration Management - Command-line argument parsing and configuration loading

When to Use This Module

SiLA2.Client is the standard choice for building SiLA2 client applications that connect to servers using compile-time generated gRPC stubs.

Scenario Use SiLA2.Client Use SiLA2.Client.Dynamic
Building client for known features ✅ Yes (compile-time stubs) ❌ No (unnecessary overhead)
Type-safe feature access ✅ Yes (IntelliSense support) ⚠️ Limited (reflection-based)
Performance-critical applications ✅ Yes (compiled code) ⚠️ Slower (runtime generation)
Automatic server discovery needed ✅ Yes (mDNS built-in) ✅ Yes (via SiLA2.Client)
Universal client (any feature) ❌ No ✅ Yes (runtime discovery)
Testing tools for unknown features ❌ No ✅ Yes

Choose SiLA2.Client when:

  • You know which SiLA2 features you'll communicate with at compile time
  • You want optimal performance and type safety
  • You're building production client applications
  • You need mDNS server discovery on the local network

Choose SiLA2.Client.Dynamic when:

  • You need to connect to any SiLA2 server without pre-compiling feature definitions
  • You're building universal tools or testing utilities
  • Runtime feature discovery is more important than performance

Installation

Install via NuGet Package Manager:

dotnet add package SiLA2.Client

Or via Package Manager Console:

Install-Package SiLA2.Client

Requirements

  • .NET 10.0+
  • SiLA2.Core (automatically installed as dependency)
  • Microsoft.Extensions.DependencyInjection 10.0.2+ (automatically installed)
  • Microsoft.Extensions.Logging 10.0.2+ (automatically installed)

Quick Start

Get connected to a SiLA2 server in 5 minutes.

1. Create a Console Application

dotnet new console -n MySiLA2Client
cd MySiLA2Client
dotnet add package SiLA2.Client
dotnet add package Microsoft.Extensions.Configuration.Json

2. Add Configuration File

Create appsettings.json:

{
  "ClientConfig": {
    "IpOrCdirOrFullyQualifiedHostName": "localhost",
    "Port": 50051,
    "DiscoveryServiceName": "_sila._tcp.local.",
    "NetworkInterface": "0.0.0.0"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

3. Discover Servers and Connect

using Microsoft.Extensions.Configuration;
using SiLA2.Client;
using System;
using System.Linq;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // Load configuration
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", optional: false)
            .Build();

        // Initialize configurator
        var configurator = new Configurator(configuration, args);

        // Discover servers on the network
        Console.WriteLine("Searching for SiLA2 servers...");
        var servers = await configurator.SearchForServers();

        if (servers.Count == 0)
        {
            Console.WriteLine("No servers found.");
            return;
        }

        // Display discovered servers
        foreach (var server in servers.Values)
        {
            Console.WriteLine($"Found: {server.ServerName} at {server.Address}:{server.Port}");
        }

        // Connect to first server
        var targetServer = servers.Values.First();
        var channel = await configurator.GetChannel(
            targetServer.Address,
            targetServer.Port,
            acceptAnyServerCertificate: true);

        Console.WriteLine($"Connected to {targetServer.ServerName}");

        // Use the channel to create gRPC clients
        // var client = new MyFeature.MyFeatureClient(channel);

        await channel.ShutdownAsync();
    }
}

4. Run the Application

dotnet run

That's it! You've discovered and connected to a SiLA2 server.

Core Concepts

1. Server Discovery via mDNS

SiLA2 servers announce themselves on the network using mDNS (Multicast DNS) and DNS-SD (DNS Service Discovery). This allows clients to automatically find servers without manual configuration.

How mDNS Discovery Works
  1. Server Announcement: SiLA2 servers broadcast their presence on the network via mDNS
    • Service type: _sila._tcp.local.
    • Includes: hostname, port, server name, UUID, features
  2. Client Search: Clients query for SiLA2 services on the network
  3. Response: Servers respond with connection information
  4. Connection: Clients use the discovered information to create gRPC channels
Benefits of mDNS Discovery
  • Zero Configuration: No manual IP address entry required
  • Dynamic Networks: Servers can move between networks or change IPs
  • Service Identification: Servers advertise their implemented features
  • Laboratory Automation: Essential for plug-and-play lab instruments

2. Configurator - The Client Entry Point

The Configurator class is the primary entry point for setting up SiLA2 client applications. It performs three key functions:

  1. Dependency Injection Setup: Configures essential services (logging, network, gRPC)
  2. Server Discovery: Uses mDNS to find SiLA2 servers on the network
  3. Channel Creation: Creates configured gRPC channels for server communication

Key Services Registered by Configurator:

  • IServiceFinder - mDNS server discovery
  • IGrpcChannelProvider - gRPC channel factory
  • INetworkService - Network utilities
  • IClientConfig - Client configuration from appsettings.json

3. gRPC Channel Management

gRPC channels are the communication pathways between clients and servers. SiLA2 uses HTTP/2 and TLS encryption for secure, efficient communication.

Channel Configuration Options
Option Description Default
Host/Port Server address and port From configuration
TLS/SSL Encryption enabled Yes (HTTPS)
Certificate Validation Verify server certificates acceptAnyServerCertificate=true (dev mode)
Custom CA Custom certificate authority None (system CAs)

Production Best Practice: Always use acceptAnyServerCertificate=false with proper certificate infrastructure.

4. Connection Workflow

Typical Client Connection Flow:

1. Initialize Configurator
   ↓
2. Load Configuration (appsettings.json + command-line args)
   ↓
3. Search for Servers (mDNS discovery)
   ↓
4. Select Target Server
   ↓
5. Create gRPC Channel
   ↓
6. Instantiate Feature Client Stubs
   ↓
7. Invoke Commands/Properties
   ↓
8. Shutdown Channel

5. Command-Line Arguments

The Configurator automatically parses command-line arguments to override configuration:

# Override server host and port
dotnet run --host 192.168.1.100 --port 50052

# Override discovery settings
dotnet run --discovery-service _sila._tcp.local. --network-interface 192.168.1.0

Supported Arguments:

  • --host or -h - Server hostname or IP
  • --port or -p - Server port
  • --discovery-service - mDNS service name
  • --network-interface - Network interface for discovery

6. Binary Transfer Support

For large data transfers (files, images, AnIML documents), use the BinaryClientService:

var binaryUploadClient = new BinaryUpload.BinaryUploadClient(channel);
var binaryDownloadClient = new BinaryDownload.BinaryDownloadClient(channel);
var binaryService = new BinaryClientService(
    binaryUploadClient,
    binaryDownloadClient,
    logger);

// Upload binary data
byte[] data = File.ReadAllBytes("experiment_data.png");
string uuid = await binaryService.UploadBinary(
    data,
    chunkSize: 1024 * 1024,  // 1 MB chunks
    parameterIdentifier: "org.example.feature/Command/Parameter");

// Download binary data
byte[] downloadedData = await binaryService.DownloadBinary(uuid, chunkSize: 1024 * 1024);

Architecture & Components

Component Overview

┌─────────────────────────────────────────────────────────────┐
│                  SiLA2 Client Application                   │
│  (Console app, Desktop app, Web service)                    │
└───────────────────────────┬─────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                     Configurator                            │
│  - Initializes DI container                                 │
│  - Discovers servers via mDNS                               │
│  - Creates gRPC channels                                    │
│  - Parses command-line arguments                            │
└───────────────────────────┬─────────────────────────────────┘
                            │
            ┌───────────────┴───────────────┐
            ▼                               ▼
┌─────────────────────┐         ┌─────────────────────┐
│   IServiceFinder    │         │ IGrpcChannelProvider│
│                     │         │                     │
│ - mDNS discovery    │         │ - Channel factory   │
│ - Returns server    │         │ - TLS configuration │
│   connection info   │         │ - Certificate       │
└─────────────────────┘         │   handling          │
                                └──────────┬──────────┘
                                           │
                                           ▼
                            ┌─────────────────────────┐
                            │   GrpcChannel           │
                            │   (to SiLA2 Server)     │
                            └──────────┬──────────────┘
                                       │
                                       ▼
                        ┌─────────────────────────────────┐
                        │   Feature Client Stubs          │
                        │   (Generated from .proto)       │
                        │   - MyFeature.MyFeatureClient   │
                        └─────────────────────────────────┘

Configurator

Purpose: Central configuration and initialization for SiLA2 clients.

Key Responsibilities:

  • Dependency Injection: Sets up service container with essential services
  • Configuration Loading: Reads appsettings.json and command-line arguments
  • Server Discovery: Searches for SiLA2 servers via mDNS
  • Channel Creation: Creates secure gRPC channels with TLS

Properties:

Property Type Description
Container IServiceCollection DI container for registering services
ServiceProvider IServiceProvider Built service provider with registered services
DiscoveredServers IDictionary<Guid, ConnectionInfo> Servers found via mDNS discovery

Key Methods:

// Search for servers on the network
Task<IDictionary<Guid, ConnectionInfo>> SearchForServers();

// Create channel using client configuration
Task<GrpcChannel> GetChannel(bool acceptAnyServerCertificate = true);

// Create channel to specific server
Task<GrpcChannel> GetChannel(string host, int port, bool acceptAnyServerCertificate = true, X509Certificate2 ca = null);

// Rebuild service provider after adding services
void UpdateServiceProvider();

IServiceFinder

Purpose: Discovers SiLA2 servers using mDNS/DNS-SD.

Key Method:

Task<IEnumerable<ConnectionInfo>> GetConnections(
    string serviceName,      // "_sila._tcp.local."
    string networkInterface, // "0.0.0.0" for all interfaces
    int timeout);            // Search duration in milliseconds

ConnectionInfo Structure:

public class ConnectionInfo
{
    public string Address { get; set; }           // Hostname or IP
    public int Port { get; set; }                 // gRPC port
    public string ServerName { get; set; }        // Display name
    public string ServerUuid { get; set; }        // Unique identifier
    public string ServerType { get; set; }        // Server type identifier
    public string ServerInfo { get; set; }        // Additional metadata
    public SilaCA SilaCA { get; set; }           // Certificate authority
}

IGrpcChannelProvider

Purpose: Creates and configures gRPC channels for server communication.

Key Method:

Task<GrpcChannel> GetChannel(
    string host,
    int port,
    bool acceptAnyServerCertificate = true,
    X509Certificate2 ca = null);

Channel Configuration:

  • Protocol: HTTPS (HTTP/2 over TLS)
  • Max Message Size: Configured for SiLA2 (default gRPC limits apply)
  • Keep-Alive: Configured for long-running connections
  • Certificate Validation: Configurable via acceptAnyServerCertificate

BinaryClientService

Purpose: Handles large binary data transfers using chunked streaming.

Key Methods:

// Upload binary data in chunks
Task<string> UploadBinary(byte[] value, int chunkSize, string parameterIdentifier);

// Download binary data in chunks
Task<byte[]> DownloadBinary(string binaryTransferUuid, int chunkSize);

How Chunked Transfer Works:

  1. Upload:

    • Client creates binary transfer on server
    • Splits data into chunks (e.g., 1 MB each)
    • Uploads chunks via bidirectional streaming
    • Server acknowledges each chunk
    • Returns UUID for referencing uploaded data
  2. Download:

    • Client requests binary metadata (size, chunk count)
    • Requests chunks sequentially
    • Server streams chunks back
    • Client assembles chunks into complete byte array

Use Case: Transferring AnIML documents, images, or large datasets.

Usage Examples

Example 1: Basic Server Discovery and Connection

using Microsoft.Extensions.Configuration;
using SiLA2.Client;
using System;
using System.Linq;
using System.Threading.Tasks;

public class BasicClient
{
    public static async Task Main(string[] args)
    {
        // Setup configuration
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

        // Initialize configurator
        var configurator = new Configurator(configuration, args);

        // Discover servers
        var servers = await configurator.SearchForServers();

        if (servers.Count == 0)
        {
            Console.WriteLine("No SiLA2 servers found on the network.");
            return;
        }

        // Display all discovered servers
        Console.WriteLine($"Found {servers.Count} server(s):");
        foreach (var server in servers.Values)
        {
            Console.WriteLine($"  - {server.ServerName} ({server.ServerType})");
            Console.WriteLine($"    Address: {server.Address}:{server.Port}");
            Console.WriteLine($"    UUID: {server.ServerUuid}");
        }

        // Connect to first server
        var targetServer = servers.Values.First();
        var channel = await configurator.GetChannel(
            targetServer.Address,
            targetServer.Port,
            acceptAnyServerCertificate: true);

        Console.WriteLine($"Connected to {targetServer.ServerName}");

        // Create feature client stubs here
        // var client = new TemperatureController.TemperatureControllerClient(channel);

        // Cleanup
        await channel.ShutdownAsync();
    }
}

Example 2: Filtering Discovered Servers

using SiLA2.Client;
using System;
using System.Linq;
using System.Threading.Tasks;

public class FilteredDiscovery
{
    public static async Task<GrpcChannel> ConnectToTemperatureServer(Configurator configurator)
    {
        // Discover all servers
        var servers = await configurator.SearchForServers();

        // Filter by server type
        var tempServer = servers.Values
            .FirstOrDefault(s => s.ServerType == "SiLA2TemperatureServer");

        if (tempServer == null)
        {
            throw new Exception("Temperature server not found on network");
        }

        Console.WriteLine($"Found Temperature Server: {tempServer.ServerName}");

        // Connect with server's own certificate authority
        return await configurator.GetChannel(
            tempServer.Address,
            tempServer.Port,
            acceptAnyServerCertificate: false,
            ca: tempServer.SilaCA.GetCaFromFormattedCa());
    }
}

Example 3: Manual Server Connection (No Discovery)

using SiLA2.Client;
using System.Threading.Tasks;

public class ManualConnection
{
    public static async Task<GrpcChannel> ConnectManually()
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

        var configurator = new Configurator(configuration, new string[] { });

        // Connect directly to known server without mDNS
        var channel = await configurator.GetChannel(
            host: "192.168.1.100",
            port: 50051,
            acceptAnyServerCertificate: true);

        Console.WriteLine("Connected to server at 192.168.1.100:50051");

        return channel;
    }
}

Example 4: Calling Unobservable Commands

using Sila2.Org.Silastandard.Protobuf;
using TemperatureController = Sila2.Org.Silastandard.Examples.TemperatureController.V1;
using System.Threading.Tasks;

public class UnobservableCommandExample
{
    public static async Task SetTemperature(GrpcChannel channel, double temperatureKelvin)
    {
        // Create client stub
        var client = new TemperatureController.TemperatureController.TemperatureControllerClient(channel);

        // Prepare request
        var request = new TemperatureController.SetTargetTemperature_Parameters
        {
            Temperature = new Real { Value = temperatureKelvin }
        };

        // Call command (returns immediately)
        var response = await client.SetTargetTemperatureAsync(request);

        Console.WriteLine("Target temperature set successfully");
    }
}

Example 5: Calling Observable Commands

using Sila2.Org.Silastandard;
using Sila2.Org.Silastandard.Protobuf;
using TemperatureController = Sila2.Org.Silastandard.Examples.TemperatureController.V1;
using System;
using System.Threading;
using System.Threading.Tasks;

public class ObservableCommandExample
{
    public static async Task ControlTemperature(GrpcChannel channel, double targetTemp)
    {
        var client = new TemperatureController.TemperatureController.TemperatureControllerClient(channel);

        // 1. Initiate observable command
        var request = new TemperatureController.ControlTemperature_Parameters
        {
            Temperature = new Real { Value = targetTemp }
        };

        var confirmation = await client.ControlTemperatureAsync(request);
        var commandUuid = confirmation.CommandExecutionUUID;

        Console.WriteLine($"Command initiated: {commandUuid.Value}");

        // 2. Subscribe to ExecutionInfo (progress updates)
        var infoRequest = new Subscribe_Parameters
        {
            CommandExecutionUUID = commandUuid
        };

        using var infoStream = client.ControlTemperature_Info(infoRequest);
        var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token;

        await foreach (var executionInfo in infoStream.ResponseStream.ReadAllAsync(cancellationToken))
        {
            Console.WriteLine($"Status: {executionInfo.CommandStatus}");
            Console.WriteLine($"Progress: {executionInfo.ProgressInfo?.Value * 100:F1}%");

            if (executionInfo.CommandStatus == ExecutionInfo.Types.CommandStatus.FinishedSuccessfully)
            {
                Console.WriteLine("Command completed successfully!");
                break;
            }
            else if (executionInfo.CommandStatus == ExecutionInfo.Types.CommandStatus.FinishedWithError)
            {
                Console.WriteLine($"Command failed: {executionInfo.Message?.Value}");
                throw new Exception("Command execution failed");
            }
        }

        // 3. Retrieve final result
        var resultRequest = new CommandExecutionUUID { Value = commandUuid.Value };
        var result = await client.ControlTemperature_ResultAsync(resultRequest);

        Console.WriteLine("Temperature control completed");
    }
}

Example 6: Reading Unobservable Properties

using TemperatureController = Sila2.Org.Silastandard.Examples.TemperatureController.V1;
using System;
using System.Threading.Tasks;

public class UnobservablePropertyExample
{
    public static async Task ReadTemperatureRange(GrpcChannel channel)
    {
        var client = new TemperatureController.TemperatureController.TemperatureControllerClient(channel);

        // Read property (returns immediately)
        var response = await client.Get_TemperatureRangeAsync(new Google.Protobuf.WellKnownTypes.Empty());

        Console.WriteLine($"Min Temperature: {response.MinTemperature.Value} K");
        Console.WriteLine($"Max Temperature: {response.MaxTemperature.Value} K");
    }
}

Example 7: Subscribing to Observable Properties

using TemperatureController = Sila2.Org.Silastandard.Examples.TemperatureController.V1;
using System;
using System.Threading;
using System.Threading.Tasks;

public class ObservablePropertyExample
{
    public static async Task MonitorCurrentTemperature(GrpcChannel channel, TimeSpan duration)
    {
        var client = new TemperatureController.TemperatureController.TemperatureControllerClient(channel);

        // Subscribe to property updates
        var request = new Google.Protobuf.WellKnownTypes.Empty();
        using var stream = client.Subscribe_CurrentTemperature(request);

        var cancellationToken = new CancellationTokenSource(duration).Token;

        Console.WriteLine($"Monitoring temperature for {duration.TotalSeconds} seconds...");

        try
        {
            await foreach (var update in stream.ResponseStream.ReadAllAsync(cancellationToken))
            {
                Console.WriteLine($"Current Temperature: {update.CurrentTemperature.Value} K");
            }
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Monitoring stopped (timeout reached)");
        }
    }
}

Example 8: Working with Metadata

using Grpc.Core;
using Sila2.Org.Silastandard.Protobuf;
using System;
using System.Text;
using System.Threading.Tasks;

public class MetadataExample
{
    public static async Task CallWithMetadata(GrpcChannel channel)
    {
        var client = new MyFeature.MyFeatureClient(channel);

        // Prepare metadata
        var metadata = new Metadata();
        metadata.Add("client-id", "my-client-app");
        metadata.Add("user", Convert.ToBase64String(Encoding.UTF8.GetBytes("john.doe")));
        metadata.Add("session", Guid.NewGuid().ToString());

        // Call with metadata
        var request = new MyCommand_Parameters
        {
            Parameter1 = new String { Value = "test" }
        };

        var response = await client.MyCommandAsync(request, metadata);

        // Extract response metadata
        var responseHeaders = response.GetTrailers();
        if (responseHeaders != null)
        {
            foreach (var entry in responseHeaders)
            {
                Console.WriteLine($"Response metadata: {entry.Key} = {entry.Value}");
            }
        }
    }
}

Example 9: Binary Upload

using SiLA2.Client;
using System;
using System.IO;
using System.Threading.Tasks;

public class BinaryUploadExample
{
    public static async Task UploadFile(GrpcChannel channel, string filePath)
    {
        // Create binary service
        var binaryUploadClient = new Sila2.Org.Silastandard.BinaryUpload.BinaryUploadClient(channel);
        var binaryDownloadClient = new Sila2.Org.Silastandard.BinaryDownload.BinaryDownloadClient(channel);
        var binaryService = new BinaryClientService(
            binaryUploadClient,
            binaryDownloadClient,
            logger);

        // Read file
        byte[] fileData = await File.ReadAllBytesAsync(filePath);
        Console.WriteLine($"Uploading {fileData.Length} bytes...");

        // Upload in 1 MB chunks
        string transferUuid = await binaryService.UploadBinary(
            value: fileData,
            chunkSize: 1024 * 1024,
            parameterIdentifier: "org.example.feature/UploadData/FileData");

        Console.WriteLine($"Upload complete. Binary UUID: {transferUuid}");

        // Use the UUID in a command parameter
        // var request = new UploadData_Parameters
        // {
        //     FileData = new Binary { BinaryTransferUUID = transferUuid }
        // };
    }
}

Example 10: Complete Temperature Client Example

Based on src/Examples/TemperatureController/SiLA2.Temperature.Client.App/Program.cs:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SiLA2.Client;
using Serilog;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using TemperatureController = Sila2.Org.Silastandard.Examples.TemperatureController.V1;

class Program
{
    static async Task Main(string[] args)
    {
        // Load configuration
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .Build();

        // Initialize configurator
        var configurator = new Configurator(configuration, args);

        // Setup logging
        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .CreateLogger();

        configurator.Container.AddLogging(x =>
        {
            x.ClearProviders();
            x.AddSerilog(dispose: true);
        });
        configurator.UpdateServiceProvider();

        var logger = configurator.ServiceProvider.GetRequiredService<ILogger<Program>>();

        // Discover servers
        logger.LogInformation("Starting Server Discovery...");
        var serverMap = await configurator.SearchForServers();

        // Connect to Temperature server
        GrpcChannel channel;
        var serverType = "SiLA2TemperatureServer";
        var server = serverMap.Values.FirstOrDefault(x => x.ServerType == serverType);

        if (server != null)
        {
            logger.LogInformation("Found Server");
            logger.LogInformation(server.ServerInfo);
            logger.LogInformation($"Connecting to {server}");

            channel = await configurator.GetChannel(
                server.Address,
                server.Port,
                acceptAnyServerCertificate: false,
                ca: server.SilaCA.GetCaFromFormattedCa());
        }
        else
        {
            logger.LogInformation("No server discovered. Using configuration fallback.");
            channel = await configurator.GetChannel(acceptAnyServerCertificate: true);
        }

        // Create client
        logger.LogInformation("Initializing Client...");
        var client = new TemperatureController.TemperatureController.TemperatureControllerClient(channel);

        // Call commands and properties
        // ... (see full example in repository)

        // Cleanup
        logger.LogInformation("Shutting down connection...");
        await channel.ShutdownAsync();

        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }
}

Configuration

appsettings.json Configuration

Complete Example:

{
  "ClientConfig": {
    "IpOrCdirOrFullyQualifiedHostName": "localhost",
    "Port": 50051,
    "DiscoveryServiceName": "_sila._tcp.local.",
    "NetworkInterface": "0.0.0.0",
    "Timeout": 30000
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "SiLA2.Client": "Debug",
      "Grpc": "Warning"
    }
  }
}

Configuration Properties:

Property Type Default Description
IpOrCdirOrFullyQualifiedHostName string "localhost" Server hostname or IP address
Port int 50051 Server gRPC port
DiscoveryServiceName string "_sila._tcp.local." mDNS service name for discovery
NetworkInterface string "0.0.0.0" Network interface for mDNS (0.0.0.0 = all)
Timeout int 30000 Connection timeout in milliseconds

Command-Line Arguments

Override configuration via command-line:

# Override server connection
dotnet run --host 192.168.1.100 --port 50052

# Override discovery settings
dotnet run --discovery-service _sila._tcp.local. --network-interface 192.168.1.0

# Combine multiple arguments
dotnet run --host localhost --port 50051 --discovery-service _sila._tcp.local.

Argument Precedence: Command-line args > appsettings.json > defaults

Dependency Injection Setup

Minimal Setup:

var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

var configurator = new Configurator(configuration, args);

// Services are auto-registered:
// - IServiceFinder
// - IGrpcChannelProvider
// - INetworkService
// - IClientConfig

Custom Services:

var configurator = new Configurator(configuration, args);

// Add custom services
configurator.Container.AddSingleton<IMyService, MyService>();
configurator.Container.AddScoped<IRepository, Repository>();

// Rebuild service provider
configurator.UpdateServiceProvider();

// Access services
var myService = configurator.ServiceProvider.GetRequiredService<IMyService>();

Security & Certificates

TLS/SSL Configuration

SiLA2 uses HTTPS (HTTP/2 over TLS) for all gRPC communication.

Development Mode (Self-Signed Certificates)
// Accept any server certificate (DEVELOPMENT ONLY)
var channel = await configurator.GetChannel(
    host: "localhost",
    port: 50051,
    acceptAnyServerCertificate: true);  // ⚠️ Insecure - dev only

Security Warning: This disables certificate validation. Use only in development/testing.

Production Mode (Proper Certificates)
// Use server's own CA certificate
var channel = await configurator.GetChannel(
    server.Address,
    server.Port,
    acceptAnyServerCertificate: false,  // ✅ Validate certificates
    ca: server.SilaCA.GetCaFromFormattedCa());
Custom Certificate Authority
// Load custom CA certificate
var caCert = new X509Certificate2("path/to/ca-certificate.pem");

var channel = await configurator.GetChannel(
    "myserver.example.com",
    50051,
    acceptAnyServerCertificate: false,
    ca: caCert);

Certificate Validation Callbacks

For advanced certificate validation:

var httpHandler = new HttpClientHandler();
httpHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
    // Custom validation logic
    if (errors == SslPolicyErrors.None)
        return true;

    // Log certificate details
    Console.WriteLine($"Certificate subject: {cert.Subject}");
    Console.WriteLine($"Certificate issuer: {cert.Issuer}");
    Console.WriteLine($"Errors: {errors}");

    // Accept specific certificate thumbprints
    var trustedThumbprints = new[] { "ABC123...", "DEF456..." };
    return trustedThumbprints.Contains(cert.GetCertHashString());
};

var channelOptions = new GrpcChannelOptions
{
    HttpHandler = httpHandler
};

var channel = GrpcChannel.ForAddress("https://myserver:50051", channelOptions);

Production Certificate Setup

Recommended Approach:

  1. Use Valid SSL Certificates: Obtain certificates from a trusted CA (Let's Encrypt, commercial CA)
  2. Enable Certificate Validation: Set acceptAnyServerCertificate=false
  3. Certificate Pinning (Optional): Validate specific certificate thumbprints
  4. Mutual TLS (Optional): Client certificates for authentication

Example Production Configuration:

var channel = await configurator.GetChannel(
    host: "sila-server.production.com",
    port: 443,  // Standard HTTPS port
    acceptAnyServerCertificate: false,
    ca: null);  // Use system-trusted CAs

Error Handling

gRPC Status Codes

Handle common gRPC errors:

using Grpc.Core;
using System;
using System.Threading.Tasks;

public class ErrorHandlingExample
{
    public static async Task CallCommandWithErrorHandling(GrpcChannel channel)
    {
        try
        {
            var client = new MyFeature.MyFeatureClient(channel);
            var response = await client.MyCommandAsync(request);
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
        {
            Console.WriteLine("Server is unavailable. Check network connection.");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
        {
            Console.WriteLine("Request timed out. Server may be overloaded.");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
        {
            Console.WriteLine("Authentication failed. Check credentials.");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.PermissionDenied)
        {
            Console.WriteLine("Access denied. Insufficient permissions.");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
        {
            Console.WriteLine($"Invalid parameter: {ex.Status.Detail}");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.FailedPrecondition)
        {
            // SiLA2 defined execution error
            Console.WriteLine($"Command execution error: {ex.Status.Detail}");

            // Parse SiLA2 error metadata
            var errorType = ex.Trailers.GetValue("sila2-error-type");
            var errorIdentifier = ex.Trailers.GetValue("sila2-error-identifier");
            Console.WriteLine($"Error type: {errorType}");
            Console.WriteLine($"Error ID: {errorIdentifier}");
        }
        catch (RpcException ex)
        {
            Console.WriteLine($"gRPC error: {ex.StatusCode} - {ex.Status.Detail}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Unexpected error: {ex.Message}");
        }
    }
}

Network Error Handling

using System;
using System.Net.Sockets;
using System.Threading.Tasks;

public class NetworkErrorExample
{
    public static async Task<GrpcChannel> ConnectWithRetry(Configurator configurator, int maxRetries = 3)
    {
        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                Console.WriteLine($"Connection attempt {attempt}/{maxRetries}...");
                return await configurator.GetChannel(acceptAnyServerCertificate: true);
            }
            catch (SocketException ex)
            {
                Console.WriteLine($"Network error: {ex.Message}");

                if (attempt == maxRetries)
                    throw;

                await Task.Delay(TimeSpan.FromSeconds(2 * attempt)); // Exponential backoff
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Unexpected error: {ex.Message}");
                throw;
            }
        }

        throw new Exception("Failed to connect after maximum retries");
    }
}

Timeout Handling

using Grpc.Core;
using System;
using System.Threading;
using System.Threading.Tasks;

public class TimeoutExample
{
    public static async Task CallWithTimeout(GrpcChannel channel, TimeSpan timeout)
    {
        var client = new MyFeature.MyFeatureClient(channel);

        var cancellationToken = new CancellationTokenSource(timeout).Token;
        var deadline = DateTime.UtcNow.Add(timeout);

        try
        {
            var callOptions = new CallOptions(
                deadline: deadline,
                cancellationToken: cancellationToken);

            var response = await client.MyCommandAsync(request, callOptions);
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
        {
            Console.WriteLine($"Operation timed out after {timeout.TotalSeconds} seconds");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Operation was cancelled");
        }
    }
}

Retry Strategies

using Polly;
using System;
using System.Threading.Tasks;

public class RetryExample
{
    public static async Task CallWithRetry(GrpcChannel channel)
    {
        var retryPolicy = Policy
            .Handle<RpcException>(ex => ex.StatusCode == StatusCode.Unavailable)
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
                onRetry: (exception, timespan, attempt, context) =>
                {
                    Console.WriteLine($"Retry {attempt} after {timespan.TotalSeconds}s due to: {exception.Message}");
                });

        await retryPolicy.ExecuteAsync(async () =>
        {
            var client = new MyFeature.MyFeatureClient(channel);
            var response = await client.MyCommandAsync(request);
        });
    }
}

Advanced Topics

Custom DI Service Registration

using Microsoft.Extensions.DependencyInjection;
using SiLA2.Client;

public class CustomDIExample
{
    public static void ConfigureServices(Configurator configurator)
    {
        // Add custom singleton services
        configurator.Container.AddSingleton<IDeviceManager, DeviceManager>();
        configurator.Container.AddSingleton<IDataLogger, FileDataLogger>();

        // Add scoped services (per-request lifetime)
        configurator.Container.AddScoped<IRepository, DatabaseRepository>();

        // Add HTTP client for external APIs
        configurator.Container.AddHttpClient<IExternalApi, ExternalApiClient>();

        // Add options pattern
        configurator.Container.Configure<MyOptions>(options =>
        {
            options.Setting1 = "value1";
            options.Setting2 = 42;
        });

        // Rebuild service provider
        configurator.UpdateServiceProvider();

        // Access services
        var deviceManager = configurator.ServiceProvider.GetRequiredService<IDeviceManager>();
    }
}

Multiple Server Connections

using SiLA2.Client;
using System.Collections.Generic;
using System.Threading.Tasks;

public class MultiServerExample
{
    public static async Task ConnectToMultipleServers(Configurator configurator)
    {
        var servers = await configurator.SearchForServers();

        var channels = new Dictionary<string, GrpcChannel>();

        foreach (var server in servers.Values)
        {
            var channel = await configurator.GetChannel(
                server.Address,
                server.Port,
                acceptAnyServerCertificate: true);

            channels[server.ServerName] = channel;
        }

        // Use channels
        foreach (var (name, channel) in channels)
        {
            Console.WriteLine($"Connected to {name}");

            // Create feature clients
            // var client = new MyFeature.MyFeatureClient(channel);
        }

        // Cleanup
        foreach (var channel in channels.Values)
        {
            await channel.ShutdownAsync();
        }
    }
}

Connection Pooling

using Grpc.Net.Client;
using System.Collections.Concurrent;
using System.Threading.Tasks;

public class ConnectionPool
{
    private readonly ConcurrentDictionary<string, GrpcChannel> _channels = new();
    private readonly Configurator _configurator;

    public ConnectionPool(Configurator configurator)
    {
        _configurator = configurator;
    }

    public async Task<GrpcChannel> GetOrCreateChannel(string host, int port)
    {
        var key = $"{host}:{port}";

        return _channels.GetOrAdd(key, async _ =>
        {
            return await _configurator.GetChannel(host, port, acceptAnyServerCertificate: true);
        }).Result;
    }

    public async Task CloseAll()
    {
        foreach (var channel in _channels.Values)
        {
            await channel.ShutdownAsync();
        }
        _channels.Clear();
    }
}

Health Checks

using Grpc.Core;
using Grpc.Health.V1;
using System;
using System.Threading.Tasks;

public class HealthCheckExample
{
    public static async Task<bool> CheckServerHealth(GrpcChannel channel)
    {
        try
        {
            var healthClient = new Health.HealthClient(channel);
            var response = await healthClient.CheckAsync(new HealthCheckRequest());

            return response.Status == HealthCheckResponse.Types.ServingStatus.Serving;
        }
        catch (RpcException ex)
        {
            Console.WriteLine($"Health check failed: {ex.Status.Detail}");
            return false;
        }
    }

    public static async Task MonitorServerHealth(GrpcChannel channel, TimeSpan interval)
    {
        var healthClient = new Health.HealthClient(channel);

        using var watchCall = healthClient.Watch(new HealthCheckRequest());

        await foreach (var response in watchCall.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine($"Server health: {response.Status}");

            if (response.Status != HealthCheckResponse.Types.ServingStatus.Serving)
            {
                Console.WriteLine("Server is unhealthy!");
            }
        }
    }
}

Metadata Extraction

using Grpc.Core;
using System;
using System.Text;
using System.Threading.Tasks;

public class MetadataExtractor
{
    public static async Task ExtractMetadata(GrpcChannel channel)
    {
        var client = new MyFeature.MyFeatureClient(channel);

        // Create call with headers
        var headers = new Metadata();
        headers.Add("custom-header", "value");

        var call = client.MyCommandAsync(request, headers);

        // Get response headers (sent before response)
        var responseHeaders = await call.ResponseHeadersAsync;
        Console.WriteLine("Response Headers:");
        foreach (var header in responseHeaders)
        {
            Console.WriteLine($"  {header.Key}: {header.Value}");
        }

        // Get response
        var response = await call.ResponseAsync;

        // Get trailers (sent after response)
        var trailers = call.GetTrailers();
        Console.WriteLine("Response Trailers:");
        foreach (var trailer in trailers)
        {
            Console.WriteLine($"  {trailer.Key}: {trailer.Value}");
        }
    }
}

Working Examples

The repository includes complete working client examples demonstrating real-world usage patterns.

Temperature Controller Client

Location: src/Examples/TemperatureController/SiLA2.Temperature.Client.App/

Run:

dotnet run --project src/Examples/TemperatureController/SiLA2.Temperature.Client.App/SiLA2.Temperature.Client.App.csproj

Features Demonstrated:

  • Automatic server discovery via mDNS
  • Fallback to manual configuration
  • Certificate handling (server's own CA)
  • Calling unobservable commands (SetTargetTemperature)
  • Calling observable commands (ControlTemperature) with progress tracking
  • Subscribing to observable properties (CurrentTemperature)
  • Error handling (parameter validation, defined execution errors)

Shaker Controller Client

Location: src/Examples/ShakerController/SiLA2.Shaker.Client.App/

Run:

dotnet run --project src/Examples/ShakerController/SiLA2.Shaker.Client.App/SiLA2.Shaker.Client.App.csproj

Features Demonstrated:

  • Server type filtering (connecting to specific server type)
  • Reading unobservable properties (ClampState)
  • Calling unobservable commands (OpenClamp, CloseClamp)
  • Observable command execution (Shake)
  • SiLA2 validation error handling
  • Precondition checking (clamp must be closed to shake)

Authentication Client Example

Location: src/Examples/AuthenticationAuthorization/Auth.Client.App/

Features Demonstrated:

  • Authentication with SiLA2 servers
  • Bearer token management
  • Metadata for authentication headers
  • Role-based access control

Comparison: SiLA2.Client vs SiLA2.Client.Dynamic

When to Use SiLA2.Client (Compile-Time Stubs)

Use SiLA2.Client when:

  • You know which features you'll communicate with at development time
  • You want compile-time type checking and IntelliSense
  • Performance is critical (no runtime reflection overhead)
  • You're building production client applications
  • You need maximum type safety

Benefits:

  • Type Safety: Compile-time checking prevents errors
  • Performance: No runtime type generation overhead (~5x faster than dynamic)
  • IntelliSense: Full code completion for all features
  • Refactoring: Rename operations work correctly
  • Debugging: Standard debugging with breakpoints and watches

Drawbacks:

  • Recompilation: Must recompile when features change
  • Feature Coupling: Client code coupled to specific feature versions
  • Binary Size: Generated stubs increase assembly size

When to Use SiLA2.Client.Dynamic (Runtime Generation)

Use SiLA2.Client.Dynamic when:

  • You need to connect to any SiLA2 server without pre-compiling features
  • You're building universal testing tools or debugging utilities
  • Runtime feature discovery is more important than performance
  • You want to avoid managing feature assemblies

Benefits:

  • Flexibility: Works with any SiLA2 feature at runtime
  • No Recompilation: Load new features without rebuilding
  • Smaller Binaries: No generated code in assembly
  • Universal Tools: Single client for all servers

Drawbacks:

  • No Type Safety: Runtime errors instead of compile errors
  • Limited IntelliSense: Dynamic types don't provide code completion
  • Performance Overhead: Reflection-based calls (~5x slower)
  • Complex Debugging: Dynamic types harder to inspect

Performance Comparison

Operation SiLA2.Client SiLA2.Client.Dynamic Winner
Command call overhead ~0.5ms ~2-5ms ✅ SiLA2.Client (5-10x faster)
Property read overhead ~0.3ms ~1-3ms ✅ SiLA2.Client
Feature loading time Build-time ~50-200ms ✅ SiLA2.Client
Binary size Larger (+stubs) Smaller ✅ SiLA2.Client.Dynamic
Type safety Compile-time Runtime ✅ SiLA2.Client

Note: For most SiLA2 operations (which involve I/O and device communication), the overhead difference is negligible compared to the operation duration (seconds to minutes).

Hybrid Approach

Use both libraries in the same application:

// Use SiLA2.Client for known features (performance-critical)
var tempClient = new TemperatureController.TemperatureControllerClient(channel);
var temp = await tempClient.Get_CurrentTemperatureAsync(new Empty());

// Use SiLA2.Client.Dynamic for unknown features (flexibility)
var dynamicService = new DynamicMessageService(payloadFactory);
var unknownFeature = silaServer.ReadFeature("UnknownFeature-v1_0.sila.xml");
var result = dynamicService.GetUnobservableProperty("SomeProperty", channel, unknownFeature);

API Reference Summary

IConfigurator

public interface IConfigurator
{
    // Dependency injection
    IServiceCollection Container { get; }
    IServiceProvider ServiceProvider { get; }
    void UpdateServiceProvider();

    // Server discovery
    IDictionary<Guid, ConnectionInfo> DiscoveredServers { get; }
    Task<IDictionary<Guid, ConnectionInfo>> SearchForServers();

    // Channel creation
    Task<GrpcChannel> GetChannel(bool acceptAnyServerCertificate = true);
    Task<GrpcChannel> GetChannel(string host, int port, bool acceptAnyServerCertificate = true, X509Certificate2 ca = null);
}

Configurator

public class Configurator : IConfigurator
{
    public Configurator(IConfiguration configuration, string[] args);

    public IServiceCollection Container { get; }
    public IServiceProvider ServiceProvider { get; private set; }
    public IDictionary<Guid, ConnectionInfo> DiscoveredServers { get; }

    public Task<IDictionary<Guid, ConnectionInfo>> SearchForServers();
    public Task<GrpcChannel> GetChannel(bool acceptAnyServerCertificate = true);
    public Task<GrpcChannel> GetChannel(string host, int port, bool acceptAnyServerCertificate = true, X509Certificate2 ca = null);
    public void UpdateServiceProvider();
}

IBinaryClientService

public interface IBinaryClientService
{
    Task<string> UploadBinary(byte[] value, int chunkSize, string parameterIdentifier);
    Task<byte[]> DownloadBinary(string binaryTransferUuid, int chunkSize);
}

ConnectionInfo

public class ConnectionInfo
{
    public string Address { get; set; }
    public int Port { get; set; }
    public string ServerName { get; set; }
    public string ServerUuid { get; set; }
    public string ServerType { get; set; }
    public string ServerInfo { get; set; }
    public SilaCA SilaCA { get; set; }

    public override string ToString();
}

Core SiLA2 Packages:

  • SiLA2.Core - Core server implementation, domain models, network discovery (required dependency)
  • SiLA2.AspNetCore - ASP.NET Core integration for building SiLA2 servers
  • SiLA2.Utils - Network utilities, mDNS, configuration (included via SiLA2.Core)

Client Libraries:

Optional Modules:

Contributing & Development

This package is part of the sila_csharp project.

Building from Source

git clone --recurse-submodules https://gitlab.com/SiLA2/sila_csharp.git
cd sila_csharp/src
dotnet build SiLA2.Client/SiLA2.Client.csproj

Running Tests

# Run client integration tests
dotnet test Tests/SiLA2.Client.Tests/SiLA2.Client.Tests.csproj

# Run end-to-end tests (requires running server)
dotnet test Tests/SiLA2.IntegrationTests.Client.Tests/SiLA2.IntegrationTests.Client.Tests.csproj

Project Structure

SiLA2.Client/
├── Configurator.cs                 # Main client configuration class
├── IConfigurator.cs                # Configurator interface
├── BinaryClientService.cs          # Binary transfer implementation
├── IBinaryClientService.cs         # Binary transfer interface
├── README.md                       # This file
└── SiLA2.Client.csproj            # Project file

External Resources:

License

This project is licensed under the MIT License.

Maintainer

Christoph Pohl (@Chamundi)

Security

For security vulnerabilities, please refer to the SiLA2 Vulnerability Policy.


Questions or Issues?

  • Open an issue on GitLab
  • Join the SiLA community on Slack
  • Check the Wiki for additional documentation

Getting Started with SiLA2 Client Development?

  1. Install the package: dotnet add package SiLA2.Client
  2. Create configuration: Add appsettings.json with ClientConfig section
  3. Discover servers: Use Configurator.SearchForServers()
  4. Create channel: Use Configurator.GetChannel()
  5. Build feature clients: Reference feature assemblies and create gRPC client stubs
  6. Call commands/properties: Use generated client stub classes

Happy SiLA2 client development!

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on SiLA2.Client:

Package Downloads
SiLA2.Frontend.Razor

Web Frontend Extension for SiLA2.Server Package

SiLA2.Client.Dynamic

SiLA2.Client.Dynamic Package

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.2.2 156 2/12/2026
10.2.1 285 1/25/2026
10.2.0 687 12/23/2025
10.1.0 1,074 11/29/2025
10.0.0 1,620 11/11/2025
9.0.4 2,530 6/25/2025
9.0.3 2,099 6/21/2025
9.0.2 2,745 1/6/2025
9.0.1 2,189 11/17/2024
9.0.0 2,103 11/13/2024
8.1.2 2,330 10/20/2024
8.1.1 2,753 8/31/2024
8.1.0 3,082 2/11/2024
8.0.0 2,596 11/15/2023
7.5.4 3,972 10/27/2023
7.5.3 2,346 7/19/2023
7.5.2 2,241 7/3/2023
7.5.1 2,213 6/2/2023
7.4.6 2,196 5/21/2023
7.4.5 2,234 5/7/2023
Loading failed