SiLA2.Client
10.2.2
dotnet add package SiLA2.Client --version 10.2.2
NuGet\Install-Package SiLA2.Client -Version 10.2.2
<PackageReference Include="SiLA2.Client" Version="10.2.2" />
<PackageVersion Include="SiLA2.Client" Version="10.2.2" />
<PackageReference Include="SiLA2.Client" />
paket add SiLA2.Client --version 10.2.2
#r "nuget: SiLA2.Client, 10.2.2"
#:package SiLA2.Client@10.2.2
#addin nuget:?package=SiLA2.Client&version=10.2.2
#tool nuget:?package=SiLA2.Client&version=10.2.2
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:
- Server Discovery - Automatic detection of SiLA2 servers on the local network using mDNS/DNS-SD
- gRPC Channel Management - Creating and configuring secure gRPC channels with TLS/encryption
- Dependency Injection Setup - Simplified DI container configuration for client applications
- 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
- Server Announcement: SiLA2 servers broadcast their presence on the network via mDNS
- Service type:
_sila._tcp.local. - Includes: hostname, port, server name, UUID, features
- Service type:
- Client Search: Clients query for SiLA2 services on the network
- Response: Servers respond with connection information
- 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:
- Dependency Injection Setup: Configures essential services (logging, network, gRPC)
- Server Discovery: Uses mDNS to find SiLA2 servers on the network
- Channel Creation: Creates configured gRPC channels for server communication
Key Services Registered by Configurator:
IServiceFinder- mDNS server discoveryIGrpcChannelProvider- gRPC channel factoryINetworkService- Network utilitiesIClientConfig- 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:
--hostor-h- Server hostname or IP--portor-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:
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
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:
- Use Valid SSL Certificates: Obtain certificates from a trusted CA (Let's Encrypt, commercial CA)
- Enable Certificate Validation: Set
acceptAnyServerCertificate=false - Certificate Pinning (Optional): Validate specific certificate thumbprints
- 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();
}
Related Packages
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:
- SiLA2.Client.Dynamic - Runtime dynamic client for universal feature access
- SiLA2.Communication - Dynamic protobuf generation (used by SiLA2.Client.Dynamic)
Optional Modules:
- SiLA2.Database.SQL - Entity Framework Core SQL persistence
- SiLA2.Database.NoSQL - NoSQL persistence using LiteDB
- SiLA2.AnIML - AnIML scientific data format support
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
Links & Resources
- SiLA2 Standard: https://sila-standard.com
- Repository: https://gitlab.com/SiLA2/sila_csharp
- NuGet Package: https://www.nuget.org/packages/SiLA2.Client
- Documentation Wiki: https://gitlab.com/SiLA2/sila_csharp/-/wikis/home
- Issue Tracker: https://gitlab.com/SiLA2/sila_csharp/-/issues
- SiLA Slack: Join the community
External Resources:
- gRPC .NET: https://grpc.io/docs/languages/csharp/
- mDNS/DNS-SD: https://www.dns-sd.org/
- TLS/SSL Best Practices: https://learn.microsoft.com/aspnet/core/grpc/security
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?
- Install the package:
dotnet add package SiLA2.Client - Create configuration: Add
appsettings.jsonwith ClientConfig section - Discover servers: Use
Configurator.SearchForServers() - Create channel: Use
Configurator.GetChannel() - Build feature clients: Reference feature assemblies and create gRPC client stubs
- Call commands/properties: Use generated client stub classes
Happy SiLA2 client development!
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.3)
- Microsoft.Extensions.Logging (>= 10.0.3)
- SiLA2.Core (>= 10.2.2)
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 |