C# SDK
The C# SDK provides async/await-based transport layers for .NET applications using C# 11+ static abstract interface members.
Requirements
- .NET 7.0+ (required for static abstract interface members)
- For serial port support:
System.IO.PortsNuGet package - For network transports:
NetCoreServerNuGet package
Installation
Generate C# code (messages, framing, SDK core, and .csproj):
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/Generate with transports (includes Serial, TCP, UDP, WebSocket transport implementations):
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --csharp_sdkGeneration Options
# With custom namespace (default: StructFrame)python -m struct_frame messages.proto --build_csharp --csharp_namespace MyApp.Protocol
# With custom target framework (default: net8.0)python -m struct_frame messages.proto --build_csharp --target_framework net7.0
# With equality operatorspython -m struct_frame messages.proto --build_csharp --equality
# With transportspython -m struct_frame messages.proto --build_csharp --csharp_sdkGenerated Output Structure
Generated/├── StructFrame.csproj # Always generated├── Framework/ # Framing & SDK boilerplate│ ├── Framing/ # Frame encoders, parsers, readers, writers│ ├── Profiles/ # Frame profiles (Standard, Sensor, IPC, etc.)│ ├── Sdk/ # SDK core (StructFrameSdk, Transport base)│ │ └── Transports/ # Only with --csharp_sdk│ │ ├── SerialTransport.cs│ │ ├── TcpTransport.cs│ │ ├── UdpTransport.cs│ │ └── WebSocketTransport.cs│ └── Types/ # Base types (FrameMsgInfo, IStructFrameMessage, etc.)└── <PackageName>/ # One folder per proto package (PascalCase) ├── Enums/ # One file per enum ├── Messages/ # One file per message ├── MessageDefinitions.cs # Message registry └── SdkInterface.cs # Always generatedTransport Compilation
When --csharp_sdk is used, transport implementations are copied but excluded from compilation by default in the .csproj. Enable them at build time:
# Serial transport (System.IO.Ports)dotnet build -p:IncludeSerialTransport=true
# Network transports: TCP, UDP, WebSocket (NetCoreServer)dotnet build -p:IncludeNetCoreServer=true
# Bothdotnet build -p:IncludeSerialTransport=true -p:IncludeNetCoreServer=trueBasic Usage
The SDK client uses the unified FrameProfiles infrastructure for encoding and parsing:
using StructFrame;using StructFrame.Sdk;
// Configure the SDK with required parametersvar config = new StructFrameSdkConfig( transport: new TcpTransport("192.168.1.100", 8080), getMessageInfo: MessageDefinitions.GetMessageInfo, profile: Profiles.Standard, // optional, default is Standard debug: true // optional, default is false);
var sdk = new StructFrameSdk(config);await sdk.ConnectAsync();
// Subscribe to messages - type-safe with compile-time dispatchsdk.Subscribe<SensorDataMessage>(msg => { Console.WriteLine($"Sensor value: {msg.Value}, ID: {msg.GetMsgId()}");});
// Send messages (uses IStructFrameMessage interface)var command = new CommandMessage { Action = 1 };await sdk.SendAsync(command);
// Handle unregistered message typessdk.UnhandledMessage += frame => { Console.WriteLine($"Unknown message ID: {frame.MsgId}");};Generated SDK Interface
When you generate with --sdk, a type-safe SdkInterface class is generated for each package. This provides convenience methods for sending and subscribing to specific message types:
using StructFrame;using StructFrame.Sdk;using StructFrame.MyPackage.Sdk;
// Create the base SDKvar config = new StructFrameSdkConfig( transport: new TcpTransport("192.168.1.100", 8080), getMessageInfo: MessageDefinitions.GetMessageInfo);var sdk = new StructFrameSdk(config);
// Create the package-specific interfacevar myPackageSdk = new MyPackageSdkInterface(sdk);
// Type-safe subscribe methods for each messagemyPackageSdk.SubscribeSensorData(msg => { Console.WriteLine($"Sensor: {msg.Value}");});
myPackageSdk.SubscribeStatusUpdate(msg => { Console.WriteLine($"Status: {msg.Code}");});
// Type-safe send methods for each messageawait myPackageSdk.SendCommand(new MyPackageCommand { Action = 1 });
// Or send with individual field valuesawait myPackageSdk.SendCommand(action: 1);
// Access underlying SDK for advanced usageawait myPackageSdk.Sdk.ConnectAsync();Message Interface
Generated messages implement IStructFrameMessage<T> which provides:
public interface IStructFrameMessage<TSelf> : IStructFrameMessage where TSelf : IStructFrameMessage<TSelf>{ /// <summary> /// Deserialize a message from frame info (static abstract) /// </summary> static abstract TSelf Deserialize(FrameMsgInfo frame);}
public interface IStructFrameMessage{ ushort GetMsgId(); int GetSize(); byte[] Serialize(); (byte Magic1, byte Magic2) GetMagicNumbers();}This enables compile-time dispatch for deserialization without reflection:
// The SDK internally calls T.Deserialize(frame) directlysdk.Subscribe<SensorDataMessage>(msg => { // msg is already deserialized - no reflection needed});Message Registry
The generated code includes a MessageDefinitions class that provides:
Message Lookup by ID
using StructFrame.MyPackage;
// Get message info by ID (required for SDK configuration)var info = MessageDefinitions.GetMessageInfo(SensorDataMessage.MsgId);Console.WriteLine($"Size: {info?.Size}, Magic: {info?.Magic1:X2}{info?.Magic2:X2}");Enumerate All Messages
// Get all registered message typesforeach (var entry in MessageDefinitions.GetAllMessages()){ Console.WriteLine($"Message: {entry.Name} (ID: {entry.Id}, Size: {entry.MaxSize})");}Transports
TCP
using StructFrame.Sdk;
var transport = new TcpTransport("192.168.1.100", 8080);await transport.ConnectAsync();await transport.SendAsync(data);UDP
using StructFrame.Sdk;
var transport = new UdpTransport("192.168.1.100", 8080);await transport.ConnectAsync();await transport.SendAsync(data);Serial
The serial transport uses System.IO.Ports.SerialPort.BaseStream for reliable async reading, which is more robust than the event-based DataReceived approach.
using StructFrame.Sdk;
var config = new SerialTransportConfig{ PortName = "COM3", BaudRate = 115200, DataBits = 8, Parity = System.IO.Ports.Parity.None, StopBits = System.IO.Ports.StopBits.One};
var transport = new SerialTransport(config);await transport.ConnectAsync();await transport.SendAsync(data);Async/Await Patterns
public async Task RunAsync(){ var config = new StructFrameSdkConfig( transport: new TcpTransport("localhost", 8080), getMessageInfo: MessageDefinitions.GetMessageInfo );
var sdk = new StructFrameSdk(config);
sdk.Subscribe<StatusMessage>(HandleStatus);
await sdk.ConnectAsync();
// SDK handles incoming data automatically via transport events // Send messages as needed await sdk.SendAsync(new CommandMessage { Action = 1 });}
void HandleStatus(StatusMessage msg){ Console.WriteLine($"Received status: {msg.Code}");}Envelope Message Support
When you have envelope messages (messages with is_envelope = true), the SDK generates convenient helper methods:
using StructFrame.MyPackage.Sdk;
// Create the SDK interfacevar sdk = new MyPackageSdkInterface(baseSdk);
// Send a payload wrapped in an envelope - multiple overloads available:
// Overload 1: Pass the message object plus envelope fieldsvar adcCmd = new ADCCommand { Channel = 1, SampleRate = 1000, Enable = true };await sdk.SendADCCommandViaCommandEnvelope(adcCmd, sequenceNumber: 42, priority: 1, runImmediately: true);
// Overload 2: Pass all fields flat (payload fields + envelope fields)await sdk.SendADCCommandViaCommandEnvelope( channel: 1, sampleRate: 1000, enable: true, sequenceNumber: 42, priority: 1, runImmediately: true);The naming convention is Send{PayloadType}Via{EnvelopeName}. This pattern:
- Automatically wraps the payload in the envelope message
- Sets the discriminator field correctly (msgid or field_order)
- Handles serialization and framing
- Works with both
msgidandfield_orderdiscriminator types
.NET Platform Support
The SDK requires .NET 7.0+ due to the use of C# 11 static abstract interface members:
- .NET 7.0+
- .NET 8.0+ (default target framework)
- .NET 9.0+