Skip to content

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.Ports NuGet package

Installation

Generate with SDK:

python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --sdk

Generate with auto-generated .csproj file for immediate building:

python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --generate_csproj

.csproj Generation Options

The generator can create a .csproj file that allows immediate dotnet build:

# Basic .csproj generation (excludes SDK, no dependencies needed)
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --generate_csproj

# With custom namespace
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --generate_csproj --csharp_namespace MyApp.Protocol

# With custom target framework
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --generate_csproj --target_framework net7.0

# Full SDK with .csproj (includes System.IO.Ports dependency)
python -m struct_frame messages.proto --build_csharp --csharp_path Generated/ --sdk --generate_csproj

Basic Usage

The SDK client uses the unified FrameProfiles infrastructure for encoding and parsing:

using StructFrame;
using StructFrame.Sdk;

// Configure the SDK with required parameters
var 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 dispatch
sdk.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 types
sdk.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 SDK
var config = new StructFrameSdkConfig(
    transport: new TcpTransport("192.168.1.100", 8080),
    getMessageInfo: MessageDefinitions.GetMessageInfo
);
var sdk = new StructFrameSdk(config);

// Create the package-specific interface
var myPackageSdk = new MyPackageSdkInterface(sdk);

// Type-safe subscribe methods for each message
myPackageSdk.SubscribeSensorData(msg => {
    Console.WriteLine($"Sensor: {msg.Value}");
});

myPackageSdk.SubscribeStatusUpdate(msg => {
    Console.WriteLine($"Status: {msg.Code}");
});

// Type-safe send methods for each message
await myPackageSdk.SendCommand(new MyPackageCommand { Action = 1 });

// Or send with individual field values
await myPackageSdk.SendCommand(action: 1);

// Access underlying SDK for advanced usage
await 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) directly
sdk.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 types
foreach (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

using StructFrame.Sdk;

var transport = new SerialTransport("COM3", 115200);
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}");
}

.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+
  • .NET 9.0+