![]() |
VOOZH | about |
dotnet add package SiLA2.Client --version 10.2.4
NuGet\Install-Package SiLA2.Client -Version 10.2.4
<PackageReference Include="SiLA2.Client" Version="10.2.4" />
<PackageVersion Include="SiLA2.Client" Version="10.2.4" />Directory.Packages.props
<PackageReference Include="SiLA2.Client" />Project file
paket add SiLA2.Client --version 10.2.4
#r "nuget: SiLA2.Client, 10.2.4"
#:package SiLA2.Client@10.2.4
#addin nuget:?package=SiLA2.Client&version=10.2.4Install as a Cake Addin
#tool nuget:?package=SiLA2.Client&version=10.2.4Install as a Cake Tool
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 |
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.
This module provides four core capabilities for building SiLA2 client applications:
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:
Choose SiLA2.Client.Dynamic when:
Install via NuGet Package Manager:
dotnet add package SiLA2.Client
Or via Package Manager Console:
Install-Package SiLA2.Client
| Target Framework | Supported |
|---|---|
| .NET 10.0+ | Yes (full feature support) |
| .NET Standard 2.0 | Yes |
| .NET Framework 4.6.1+ | Yes (via netstandard2.0) |
| .NET Core 2.0+ | Yes (via netstandard2.0) |
| Mono 5.4+ | Yes (via netstandard2.0) |
| Xamarin | Yes (via netstandard2.0) |
This package multi-targets net10.0 and netstandard2.0, allowing it to be used from both modern .NET and legacy .NET Framework projects.
Get connected to a SiLA2 server in 5 minutes.
dotnet new console -n MySiLA2Client
cd MySiLA2Client
dotnet add package SiLA2.Client
dotnet add package Microsoft.Extensions.Configuration.Json
Create appsettings.json:
{
"ClientConfig": {
"IpOrCdirOrFullyQualifiedHostName": "localhost",
"Port": 50051,
"DiscoveryServiceName": "_sila._tcp.local.",
"NetworkInterface": "0.0.0.0"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
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();
}
}
dotnet run
That's it! You've discovered and connected to a SiLA2 server.
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.
_sila._tcp.local.The Configurator class is the primary entry point for setting up SiLA2 client applications. It performs three key functions:
Key Services Registered by Configurator:
IServiceFinder - mDNS server discoveryIGrpcChannelProvider - gRPC channel factoryINetworkService - Network utilitiesIClientConfig - Client configuration from appsettings.jsongRPC channels are the communication pathways between clients and servers. SiLA2 uses HTTP/2 and TLS encryption for secure, efficient communication.
| 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.
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
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 discoveryFor 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);
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
βββββββββββββββββββββββββββββββββββ
Purpose: Central configuration and initialization for SiLA2 clients.
Key Responsibilities:
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();
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
}
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:
acceptAnyServerCertificatePurpose: 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:
Download:
Use Case: Transferring AnIML documents, images, or large datasets.
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();
}
}
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());
}
}
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;
}
}
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");
}
}
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");
}
}
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");
}
}
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)");
}
}
}
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}");
}
}
}
}
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 }
// };
}
}
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();
}
}
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 |
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
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>();
SiLA2 uses HTTPS (HTTP/2 over TLS) for all gRPC communication.
// 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.
// Use server's own CA certificate
var channel = await configurator.GetChannel(
server.Address,
server.Port,
acceptAnyServerCertificate: false, // β
Validate certificates
ca: server.SilaCA.GetCaFromFormattedCa());
// 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);
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);
Recommended Approach:
acceptAnyServerCertificate=falseExample 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
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}");
}
}
}
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");
}
}
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");
}
}
}
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);
});
}
}
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>();
}
}
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();
}
}
}
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();
}
}
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!");
}
}
}
}
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}");
}
}
}
The repository includes complete working client examples demonstrating real-world usage patterns.
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:
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:
Location: src/Examples/AuthenticationAuthorization/Auth.Client.App/
Features Demonstrated:
β Use SiLA2.Client when:
Benefits:
Drawbacks:
β Use SiLA2.Client.Dynamic when:
Benefits:
Drawbacks:
| 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).
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);
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);
}
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();
}
public interface IBinaryClientService
{
Task<string> UploadBinary(byte[] value, int chunkSize, string parameterIdentifier);
Task<byte[]> DownloadBinary(string binaryTransferUuid, int chunkSize);
}
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:
Client Libraries:
Optional Modules:
This package is part of the sila_csharp project.
git clone --recurse-submodules https://gitlab.com/SiLA2/sila_csharp.git
cd sila_csharp/src
dotnet build SiLA2.Client/SiLA2.Client.csproj
# 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
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:
This project is licensed under the MIT License.
Christoph Pohl (@Chamundi)
For security vulnerabilities, please refer to the SiLA2 Vulnerability Policy.
Questions or Issues?
Getting Started with SiLA2 Client Development?
dotnet add package SiLA2.Clientappsettings.json with ClientConfig sectionConfigurator.SearchForServers()Configurator.GetChannel()Happy SiLA2 client development!
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 was computed. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 was computed. |
| .NET Framework | net461 net461 was computed. net462 net462 was computed. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 was computed. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
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 |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.2.4 | 2,124 | 3/13/2026 |
| 10.2.3 | 1,236 | 3/7/2026 |
| 10.2.2 | 2,319 | 2/12/2026 |
| 10.2.1 | 2,054 | 1/25/2026 |
| 10.2.0 | 2,425 | 12/23/2025 |
| 10.1.0 | 2,815 | 11/29/2025 |
| 10.0.0 | 3,359 | 11/11/2025 |
| 9.0.4 | 4,286 | 6/25/2025 |
| 9.0.3 | 3,837 | 6/21/2025 |
| 9.0.2 | 4,485 | 1/6/2025 |
| 9.0.1 | 3,924 | 11/17/2024 |
| 9.0.0 | 3,839 | 11/13/2024 |
| 8.1.2 | 4,118 | 10/20/2024 |
| 8.1.1 | 4,756 | 8/31/2024 |
| 8.1.0 | 4,821 | 2/11/2024 |
| 8.0.0 | 4,315 | 11/15/2023 |
| 7.5.4 | 5,839 | 10/27/2023 |
| 7.5.3 | 4,072 | 7/19/2023 |
| 7.5.2 | 3,972 | 7/3/2023 |
| 7.5.1 | 3,937 | 6/2/2023 |