![]() |
VOOZH | about |
dotnet add package SiLA2.Client.Dynamic --version 10.2.4
NuGet\Install-Package SiLA2.Client.Dynamic -Version 10.2.4
<PackageReference Include="SiLA2.Client.Dynamic" Version="10.2.4" />
<PackageVersion Include="SiLA2.Client.Dynamic" Version="10.2.4" />Directory.Packages.props
<PackageReference Include="SiLA2.Client.Dynamic" />Project file
paket add SiLA2.Client.Dynamic --version 10.2.4
#r "nuget: SiLA2.Client.Dynamic, 10.2.4"
#:package SiLA2.Client.Dynamic@10.2.4
#addin nuget:?package=SiLA2.Client.Dynamic&version=10.2.4Install as a Cake Addin
#tool nuget:?package=SiLA2.Client.Dynamic&version=10.2.4Install as a Cake Tool
SiLA2.Client.Dynamic is a .NET 10 package that enables runtime-based client development for SiLA2 servers without requiring compile-time generated gRPC stubs. This package allows developers to build generic SiLA2 client applications that can discover servers on the network, introspect their capabilities dynamically, and invoke commands and properties without pre-generating client code from .proto files.
SiLA2 (Standardization in Lab Automation) is an open connectivity standard for laboratory instruments and automation systems. It uses gRPC for communication and defines a Feature Definition Language (FDL) for describing device capabilities in XML format.
Traditional SiLA2 client development requires:
SiLA2.Client.Dynamic eliminates these requirements by:
Use this package when:
Use standard SiLA2.Client when:
Install-Package SiLA2.Client.Dynamic
dotnet add package SiLA2.Client.Dynamic
| 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.
Add appsettings.json to your project:
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Grpc": "Information",
"Microsoft": "Information"
}
},
"Connection": {
"FQHN": "localhost",
"Port": 50052,
"ServerDiscovery": {
"NIC": "",
"ServiceName": "_sila._tcp"
}
}
}
Configuration Options:
FQHN: Fallback IP address, CIDR notation, or fully qualified hostnamePort: Default port for manual connections (standard SiLA2 port is 50052)ServerDiscovery.NIC: Network interface name (e.g., "Ethernet", "eth0", "WLAN0"). Leave empty to use default interfaceServerDiscovery.ServiceName: mDNS service type suffix (standard is "_sila._tcp")using Microsoft.Extensions.Configuration;
using SiLA2.Client.Dynamic;
using SiLA2.Network.Discovery.mDNS;
// Load configuration
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
var configuration = configBuilder.Build();
// Create dynamic configurator
var client = new DynamicConfigurator(configuration, args);
Option A: Automatic Discovery via mDNS
// Search for servers on the network and retrieve their features
var serverFeatureMap = await client.GetFeatures();
// List discovered servers
foreach (var server in client.ServerFeatureMap)
{
Console.WriteLine($"Server: {server.Key.ServerName} at {server.Key.Address}:{server.Key.Port}");
foreach (var feature in server.Value)
{
Console.WriteLine($" - {feature.Key}");
}
}
Option B: Manual Connection (Known Server)
// Connect to a specific server
var connectionInfo = new ConnectionInfo("127.0.0.1", 50052);
var serverFeatureMap = await client.GetFeatures(new[] { connectionInfo });
using Grpc.Net.Client;
// Get a gRPC channel to the server
var connectionInfo = client.ServerFeatureMap.Keys.First();
var channel = await client.GetChannel(connectionInfo.Address, connectionInfo.Port);
// Find the SiLAService core feature
var silaServiceFeature = client.ServerFeatureMap[connectionInfo]
.Values.Single(x => x.FullyQualifiedIdentifier == "org.silastandard/core/SiLAService/v1");
// Call the ServerUUID property
dynamic serverUUIDResponse = client.DynamicMessageService
.GetUnobservableProperty("ServerUUID", channel, silaServiceFeature);
Console.WriteLine($"Server UUID: {serverUUIDResponse.ServerUUID.Value}");
SiLA2.Client.Dynamic follows this workflow:
1. Server Discovery (mDNS)
└─> Finds servers announcing "_sila._tcp" service
2. Feature Retrieval (gRPC call to SiLAService.Get_ImplementedFeatures)
└─> Gets list of feature identifiers (FQIs)
3. FDL Definition Retrieval (gRPC call to SiLAService.GetFeatureDefinition)
└─> Downloads XML feature definitions for each feature
4. Dynamic Type Generation (Runtime IL Emission)
└─> PayloadFactory generates protobuf message types from FDL
5. Dynamic Invocation (Reflection-based gRPC calls)
└─> DynamicMessageService invokes commands/properties using generated types
| Component | Description |
|---|---|
| DynamicConfigurator | Main entry point; handles server discovery and provides access to services |
| ServerFeatureMap | Dictionary mapping servers to their feature definitions (ConnectionInfo → Feature dictionary) |
| DynamicMessageService | Service for invoking commands and properties dynamically |
| PayloadFactory | Generates runtime protobuf message types from FDL metadata |
| Feature | Parsed representation of a SiLA2 FDL XML definition |
IDictionary<ConnectionInfo, IDictionary<string, Feature>>
↓ ↓ ↓
Server Info Feature FQI Feature Definition
// Example access pattern:
var feature = client.ServerFeatureMap[connectionInfo]["org.silastandard/core/SiLAService/v1"];
Automatic Discovery (Recommended)
// Discovers all SiLA2 servers on the network via mDNS
var serverFeatureMap = await client.GetFeatures();
Console.WriteLine($"Found {client.ServerFeatureMap.Count} servers:");
foreach (var server in client.ServerFeatureMap)
{
Console.WriteLine($"\nServer: {server.Key.ServerName}");
Console.WriteLine($" Address: {server.Key.Address}:{server.Key.Port}");
Console.WriteLine($" UUID: {server.Key.ServerUuid}");
Console.WriteLine($" Features:");
foreach (var feature in server.Value)
{
Console.WriteLine($" - {feature.Key}");
}
}
Manual Connection (Known Servers)
// Connect to specific servers when addresses are known
var connections = new[]
{
new ConnectionInfo("192.168.1.100", 50052),
new ConnectionInfo("192.168.1.101", 50052)
};
var serverFeatureMap = await client.GetFeatures(connections);
Unobservable properties return a single value immediately.
// Get a gRPC channel
var connectionInfo = client.ServerFeatureMap.Keys.First();
var channel = await client.GetChannel(connectionInfo.Address, connectionInfo.Port);
// Find the feature containing the property
var silaServiceFeature = client.ServerFeatureMap[connectionInfo]
.Values.Single(x => x.FullyQualifiedIdentifier == "org.silastandard/core/SiLAService/v1");
// Call the property using dynamic typing
dynamic serverUUIDResponse = client.DynamicMessageService
.GetUnobservableProperty("ServerUUID", channel, silaServiceFeature);
Console.WriteLine($"Server UUID: {serverUUIDResponse.ServerUUID.Value}");
// Alternative: Cast to specific protobuf type if known
dynamic serverNameResponse = client.DynamicMessageService
.GetUnobservableProperty("ServerName", channel, silaServiceFeature);
Console.WriteLine($"Server Name: {serverNameResponse.ServerName.Value}");
Output:
Server UUID: 12345678-1234-1234-1234-123456789abc
Server Name: Temperature Controller
Unobservable commands execute immediately and return a result.
// Find the feature containing the command
var testFeature = client.ServerFeatureMap[connectionInfo]
.Values.Single(x => x.FullyQualifiedIdentifier == "org.silastandard/test/UnobservableCommandTest/v1");
// Prepare parameters as a dictionary (parameter names must match FDL identifiers)
var payloadMap = new Dictionary<string, object>
{
{ "Integer", new Sila2.Org.Silastandard.Protobuf.Integer { Value = 42 } },
{ "String", new Sila2.Org.Silastandard.Protobuf.String { Value = "Hello" } }
};
// Execute the command
dynamic response = client.DynamicMessageService.ExecuteUnobservableCommand(
"JoinIntegerAndString",
channel,
testFeature,
payloadMap);
Console.WriteLine($"Joined Result: {response.JoinedParameters.Value}");
Output:
Joined Result: 42-Hello
Parameter Types: Use standard SiLA2 protobuf types from the Sila2.Org.Silastandard.Protobuf namespace:
Integer (int)Real (double)String (string)Boolean (bool)Binary (byte[])Date / Time / TimestampObservable properties stream continuous value updates.
// Find the feature with the observable property
var observablePropertyFeature = client.ServerFeatureMap[connectionInfo]
.Values.Single(x => x.FullyQualifiedIdentifier == "org.silastandard/test/ObservablePropertyTest/v1");
// Subscribe to the property stream
var propertyStream = client.DynamicMessageService.SubcribeObservableProperty(
"Alternating",
channel,
observablePropertyFeature);
// Consume the stream using async enumeration
await foreach (var value in propertyStream)
{
dynamic propertyValue = value;
Console.WriteLine($"Property value: {propertyValue.Alternating.Value}");
// You can break out of the loop to stop subscription
if (someCondition) break;
}
Output:
Property value: True
Property value: False
Property value: True
Property value: False
...
Stream Control: The subscription remains active until:
await foreach loopObservable commands are long-running operations that provide progress updates and eventual results.
// Find the feature with the observable command
var observableCommandFeature = client.ServerFeatureMap[connectionInfo]
.Values.Single(x => x.FullyQualifiedIdentifier == "org.silastandard/test/ObservableCommandTest/v1");
// Prepare parameters
var payloadMap = new Dictionary<string, object>
{
{ "N", new Sila2.Org.Silastandard.Protobuf.Integer { Value = 10 } },
{ "Delay", new Sila2.Org.Silastandard.Protobuf.Real { Value = 1.0 } }
};
// Execute the command (returns immediately with a UUID and streams)
var result = client.DynamicMessageService.ExecuteObservableCommand(
"Count",
channel,
observableCommandFeature,
payloadMap);
var confirmation = result.Item1; // CommandConfirmation (with UUID)
var executionInfoStream = result.Item2; // ExecutionInfo stream (progress)
var intermediateStream = result.Item3; // Intermediate responses (if defined)
var responseType = result.Item4; // Type for final result retrieval
Console.WriteLine($"Command started with UUID: {confirmation.CommandExecutionUUID.Value}");
// Monitor execution progress
await foreach (var executionInfo in executionInfoStream)
{
Console.WriteLine($"Status: {executionInfo.CommandStatus}");
Console.WriteLine($"Progress: {executionInfo.ProgressInfo?.Value ?? 0:P0}");
if (executionInfo.CommandStatus == ExecutionInfo.Types.CommandStatus.FinishedSuccessfully
|| executionInfo.CommandStatus == ExecutionInfo.Types.CommandStatus.FinishedWithError)
{
break;
}
}
// Retrieve the final result
dynamic finalResult = client.DynamicMessageService.GetObservableCommandResult(
confirmation.CommandExecutionUUID,
"Count",
channel,
observableCommandFeature,
responseType);
Console.WriteLine($"Final result: {finalResult.IterationResponse.Value}");
Output:
Command started with UUID: 87654321-4321-4321-4321-abcdef123456
Status: Running
Progress: 0%
Status: Running
Progress: 10%
...
Status: FinishedSuccessfully
Progress: 100%
Final result: 10
Intermediate Responses: Some observable commands define intermediate responses in their FDL for detailed progress information beyond ExecutionInfo. Access them via result.Item3 if not null.
Constructor
public DynamicConfigurator(IConfiguration configuration, string[] args)
Initializes the configurator with application configuration and command-line arguments.
Properties
ServerFeatureMap: Dictionary mapping servers to their feature definitionsDynamicMessageService: Service for invoking commands and propertiesPayloadFactory: Factory for creating dynamic protobuf typesDiscoveredServers: Dictionary of discovered servers (inherited from Configurator)Methods
// Discover servers and retrieve feature definitions
Task<IDictionary<ConnectionInfo, IDictionary<string, Feature>>> GetFeatures(
IEnumerable<ConnectionInfo> connections = null)
// Get a gRPC channel to a server
Task<GrpcChannel> GetChannel(string address, int port)
// Search for servers via mDNS (inherited from Configurator)
Task<IDictionary<Guid, ConnectionInfo>> SearchForServers(
TimeSpan? searchDuration = null)
Unobservable Property
object GetUnobservableProperty(
string propertyName,
GrpcChannel channel,
Feature feature,
Grpc.Core.Metadata metadata = null)
Observable Property
IAsyncEnumerable<object> SubcribeObservableProperty(
string propertyName,
GrpcChannel channel,
Feature feature,
Grpc.Core.Metadata metadata = null)
Unobservable Command
object ExecuteUnobservableCommand(
string operationName,
GrpcChannel channel,
Feature feature,
IDictionary<string, object> payloadMap = null,
Grpc.Core.Metadata metadata = null)
Observable Command
Tuple<CommandConfirmation, IAsyncEnumerable<ExecutionInfo>, IAsyncEnumerable<object>, Type>
ExecuteObservableCommand(
string operationName,
GrpcChannel channel,
Feature feature,
IDictionary<string, object> payloadMap = null,
Grpc.Core.Metadata metadata = null)
Observable Command Result
object GetObservableCommandResult(
CommandExecutionUUID cmdId,
string operationName,
GrpcChannel channel,
Feature feature,
Type responseType,
Grpc.Core.Metadata metadata = null)
// Generate property payload types
Tuple<Type, Type> GetPropertyPayloadTypes(Feature feature, string operation)
// Generate command payload types
Tuple<Type, Type> GetCommandPayloadTypes(Feature feature, string operation)
// Generate observable command payload types (with intermediate responses)
Tuple<Type, Type, Type> GetCommandPayloadTypesWithIntermediateCommandResponseType(
Feature feature, string operation)
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Grpc": "Debug",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Warning"
}
},
"Connection": {
"FQHN": "localhost",
"Port": 50052,
"ServerDiscovery": {
"NIC": "Ethernet",
"ServiceName": "_sila._tcp"
}
}
}
| Key | Description | Default | Examples |
|---|---|---|---|
Connection.FQHN |
Fallback host for manual connections | localhost |
192.168.1.100, temperature-server.local |
Connection.Port |
Fallback port for manual connections | 50052 |
50052 (standard SiLA2 port) |
ServerDiscovery.NIC |
Network interface for mDNS discovery | (default) | "Ethernet", "eth0", "WLAN0", "" (empty = default) |
ServerDiscovery.ServiceName |
mDNS service type suffix | _sila._tcp |
_sila._tcp (standard) |
Logging.LogLevel.Grpc |
gRPC diagnostic level | Information |
Debug (verbose), Warning (minimal) |
Windows Examples:
"Ethernet" - Wired Ethernet adapter"Wi-Fi" - Wireless adapter"vEthernet (Default Switch)" - Hyper-V virtual adapterLinux/macOS Examples:
"eth0" - Primary Ethernet"wlan0" - Wireless interface"en0" - macOS Ethernet/Wi-FiLeave empty ("") to use the system's default network interface.
| Feature | SiLA2.Client | SiLA2.Client.Dynamic |
|---|---|---|
| Proto Files Required | Yes | No |
| Compile-Time Code Gen | Yes (protoc) | No |
| Type Safety | Strong (compile-time) | Dynamic (runtime) |
| IntelliSense Support | Full | Limited (dynamic typing) |
| Runtime Flexibility | None | Full |
| Feature Discovery | Manual | Automatic |
| Performance | Fastest | Slight overhead (reflection) |
| Use Case | Production clients for known devices | Generic tools, testing, discovery apps |
| Code Changes on Feature Update | Rebuild required | None |
| Complexity | Higher initial setup | Lower initial setup |
When to Choose:
Structures (Nested Messages)
// For commands that accept structures, build nested objects
var structure = new Dictionary<string, object>
{
{ "FieldName1", new Sila2.Org.Silastandard.Protobuf.String { Value = "Value" } },
{ "FieldName2", new Sila2.Org.Silastandard.Protobuf.Integer { Value = 123 } }
};
var payloadMap = new Dictionary<string, object>
{
{ "ParameterName", structure }
};
Lists (Repeated Fields)
// For commands that accept lists/arrays
var list = new List<Sila2.Org.Silastandard.Protobuf.Integer>
{
new Sila2.Org.Silastandard.Protobuf.Integer { Value = 1 },
new Sila2.Org.Silastandard.Protobuf.Integer { Value = 2 },
new Sila2.Org.Silastandard.Protobuf.Integer { Value = 3 }
};
var payloadMap = new Dictionary<string, object>
{
{ "ValueList", list }
};
Constrained Data Types
SiLA2 supports constrained data types (e.g., Real with min/max, String with regex). The underlying protobuf types are the same; validation happens server-side:
// Even if the FDL defines constraints, use the base type
var payloadMap = new Dictionary<string, object>
{
{ "Temperature", new Sila2.Org.Silastandard.Protobuf.Real { Value = 25.5 } }
};
// Server will validate against constraints defined in FDL
Catching SiLA2 Errors
using Grpc.Core;
try
{
dynamic response = client.DynamicMessageService.ExecuteUnobservableCommand(
"SetTemperature", channel, feature, payloadMap);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
Console.WriteLine($"Invalid parameter: {ex.Status.Detail}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
Console.WriteLine($"Server unavailable: {ex.Status.Detail}");
}
catch (RpcException ex)
{
// Check for SiLA2 DefinedExecutionError in metadata
var silaError = ex.Trailers.Get("sila2-error");
if (silaError != null)
{
Console.WriteLine($"SiLA2 Error: {silaError.Value}");
}
else
{
Console.WriteLine($"gRPC Error: {ex.Message}");
}
}
Checking Observable Command Errors
await foreach (var executionInfo in executionInfoStream)
{
if (executionInfo.CommandStatus == ExecutionInfo.Types.CommandStatus.FinishedWithError)
{
Console.WriteLine($"Command failed!");
Console.WriteLine($"Error: {executionInfo.Message?.Value ?? "Unknown error"}");
break;
}
}
Caching Considerations:
GetFeatures() and stored in ServerFeatureMapPayloadFactory to avoid redundant IL emissionBest Practices:
// Good: Reuse channel
var channel = await client.GetChannel(server.Address, server.Port);
for (int i = 0; i < 100; i++)
{
var result = client.DynamicMessageService.GetUnobservableProperty(..., channel, ...);
}
// Bad: Creating new channel each time (overhead)
for (int i = 0; i < 100; i++)
{
var channel = await client.GetChannel(server.Address, server.Port);
var result = client.DynamicMessageService.GetUnobservableProperty(..., channel, ...);
}
Performance Impact:
Thread-Safe Components:
DynamicConfigurator - Safe for concurrent accessServerFeatureMap - Read-only after population, safe for concurrent readsDynamicMessageService - Stateless, safe for concurrent callsNot Thread-Safe:
// Safe: Multiple threads calling different commands
await Task.WhenAll(
Task.Run(() => client.DynamicMessageService.GetUnobservableProperty(...)),
Task.Run(() => client.DynamicMessageService.ExecuteUnobservableCommand(...))
);
A complete working example is available in the repository:
Location: src/Tests/SiLA2.IntegrationTests.DynamicClientApp/
This example demonstrates:
Run the example:
# Start the test server
dotnet run --project src/Tests/SiLA2.IntegrationTests.ServerApp
# Run the dynamic client (in another terminal)
dotnet run --project src/Tests/SiLA2.IntegrationTests.DynamicClientApp
Symptoms: GetFeatures() returns empty ServerFeatureMap
Solutions:
Check network interface:
"ServerDiscovery": {
"NIC": "Ethernet" // Try different interface names
}
Use ipconfig (Windows) or ifconfig (Linux/macOS) to list interface names
Verify server is announcing:
Start() to begin mDNS announcementFirewall/network issues:
Use manual connection:
var serverFeatureMap = await client.GetFeatures(new[] {
new ConnectionInfo("192.168.1.100", 50052)
});
Symptoms: RpcException with StatusCode.Unavailable or SSL/TLS errors
Solutions:
Development environments: Most SiLA2 servers use self-signed certificates
// Note: DynamicConfigurator handles certificate validation automatically
// For custom certificate handling, see the main repository documentation
Accept self-signed certificates: The first connection to a server may prompt a certificate warning in browsers (for web-based servers)
Symptoms: ArgumentException when calling commands/properties
Solutions:
Verify feature identifier:
// List all available features
foreach (var feature in client.ServerFeatureMap[connectionInfo].Keys)
{
Console.WriteLine(feature);
}
Check command/property name:
// Inspect feature definition
var feature = client.ServerFeatureMap[connectionInfo]["org.example/feature/MyFeature/v1"];
Console.WriteLine(feature); // Prints full FDL XML
Case sensitivity: Command and property names are case-sensitive
Symptoms: Operations hang or timeout
Solutions:
Check server is running:
netstat -an | grep 50052 # Linux/macOS
netstat -an | findstr 50052 # Windows
Increase timeout:
// Configure channel options (if needed)
var channel = GrpcChannel.ForAddress($"https://{address}:{port}", new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
}
});
Check gRPC logs:
Set "Logging.LogLevel.Grpc": "Debug" in appsettings.json
Enable verbose logging:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Grpc": "Debug",
"SiLA2": "Debug"
}
}
}
Inspect feature definitions:
// Print full FDL XML
var feature = client.ServerFeatureMap[connectionInfo]["feature.fqi"];
Console.WriteLine(feature.ToString()); // Shows XML structure
Test with official tools:
This project is open source under the MIT License. Contributions are welcome!
Repository: https://gitlab.com/SiLA2/sila_csharp
How to contribute:
See the main repository README for development setup instructions.
MIT License
This software is provided under the MIT License. See the main repository for full license text.
Christoph Pohl (@Chamundi)
For security issues, please follow the SiLA2 Vulnerability Policy.
| 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. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.2.4 | 610 | 3/13/2026 |
| 10.2.3 | 116 | 3/7/2026 |
| 10.2.2 | 192 | 2/12/2026 |
| 10.2.1 | 138 | 1/25/2026 |
| 10.2.0 | 227 | 12/23/2025 |
| 10.1.0 | 182 | 11/29/2025 |
| 10.0.0 | 348 | 11/11/2025 |
| 9.0.4 | 260 | 6/25/2025 |
| 9.0.3 | 208 | 6/21/2025 |
| 9.0.2 | 254 | 1/6/2025 |
| 9.0.1 | 241 | 11/17/2024 |
| 9.0.0 | 220 | 11/13/2024 |
| 8.1.2 | 263 | 10/20/2024 |
| 8.1.1 | 258 | 8/31/2024 |
| 8.1.0 | 835 | 2/11/2024 |
| 8.0.0 | 299 | 11/15/2023 |
| 7.5.4 | 237 | 10/27/2023 |
| 7.5.3 | 310 | 7/19/2023 |
| 7.5.2 | 282 | 7/3/2023 |
| 7.5.1 | 298 | 6/2/2023 |