Skip to content

Pattern 3: Connection Management

Start here: For overall communication architecture, see Communication Overview.

TCP devices: If your device communicates over TCP request/reply, prefer the framework TCP transport instead of raw TcpClient. See TCP Transport and TCP Request-Reply. Custom transport code is for native SDK, gateway, streaming, or protocol-specific lifecycle needs.

Primary Pattern

Implement IDeviceConnection on the root Thing. Connect returns true only after the device is authenticated, initialized, and ready for commands/events. Disconnect tears down protocol resources. The framework owns lifecycle state, retry, debouncing, metrics, and connection status publication.

public partial class Controller(Transport transport, ILogger<Controller> logger)
    : IDynamicProtocolAddressable<int>, IDeviceConnection, IConnectionAware
{
    public DynamicAddress<int> Address { get; internal set; }

    public async Task<bool> Connect(ConnectionContext context, CancellationToken ct)
    {
        logger.LogInformation(
            "Connecting to controller (attempt={Attempt}, reconnect={Reconnect})",
            context.AttemptNumber,
            context.IsReconnect);

        Address = await transport.ConnectController(this, ct);
        if (!Address.HasValue)
            return false;

        Protocol.ProtocolId = Address.Value;

        if (!await Protocol.Initialize(ct))
        {
            await transport.DisconnectController(Address.Value, ct);
            Address = DynamicAddress<int>.None;
            Protocol.ProtocolId = null;
            return false;
        }

        return true;
    }

    public async Task Disconnect(CancellationToken ct)
    {
        if (!Address.HasValue)
            return;

        Protocol.ProtocolId = null;
        await transport.DisconnectController(Address.Value, ct);
        Address = DynamicAddress<int>.None;
    }

    public Task OnConnected(ConnectionContext context)
    {
        logger.LogInformation("Controller connected");
        return Task.CompletedTask;
    }

    public Task OnDisconnected(ConnectionContext context)
    {
        logger.LogInformation("Controller disconnected");
        return Task.CompletedTask;
    }
}

Key Points

  • Return false from Connect() to trigger framework retry.
  • Return true only after authentication, initialization, address binding, and event/polling readiness are complete.
  • Do not call raw SignalConnected() or SignalDisconnected() in normal IDeviceConnection implementations; that is legacy/custom-only lifecycle plumbing.
  • Run background receive loops, alert streams, SDK event streams, or gateway monitors from services that cleanly stop on disconnect.
  • Keep long-lived services ID/address based; do not retain direct Thing references across async callbacks or timers.
  • Implement IConnectionAware when the adapter needs post-connect or post-disconnect cleanup such as stream registry clearing.

Connection Lifecycle

stateDiagram-v2
    [*] --> Offline
    Offline --> Connecting: framework calls Connect()
    Connecting --> Online: Connect() returns true
    Connecting --> Offline: Connect() returns false or throws
    Online --> Offline: Disconnect(), stream failure, watchdog, or transport loss
    Offline --> Connecting: framework retry

Custom Transport Responsibilities

For native SDK, gateway, HTTP stream, or other custom transports, document and implement:

  • external process/native library startup and shutdown
  • authentication and initialization sequence
  • event stream/message pump lifecycle
  • heartbeat/watchdog behavior
  • reconnect cleanup and stale resource cleanup
  • address binding between protocol IDs and Things
  • event replay or event loss semantics

See Also