Table of Contents

Class WebSocketConnectionActor

Namespace
Virtufin.WebSocketManager.Actors
Assembly
Virtufin.WebSocketManager.dll

One actor instance per WebSocket connection, keyed by connection id. Dapr's placement service guarantees at most one live activation of a given ActorId cluster-wide -- that guarantee, not a hand-rolled InstanceId directory + heartbeat + forward-or-reclaim pattern (Virtufin.Service.MultiInstance, removed by this migration -- see its Virtufin.Service 0.1.0 CHANGELOG entry), is what now ensures every RPC for a connection reaches the one process that actually holds its live ClientWebSocket.

A thin router, not a state owner: every method delegates straight to the pre-existing IWebSocketService, which still does the real work (open/close the socket, persist metadata via IDaprConnectionRepository) exactly as before. This mirrors the one part of the original (classic Dapr.Actors) 2026 attempt that was never the problem -- reusing existing business logic rather than duplicating it in the actor. What that attempt got wrong, and this one fixes: ConnectAsync(ConnectPayload, CancellationToken) is called immediately after the connection id is minted (see WebSocketManagerGrpcService.Connect), so the very first activation -- wherever Dapr's placement puts it -- is also where the real socket gets opened. Nothing here ever assumes "the socket is already open somewhere else."

[DaprActor("WebSocketConnection")]
public sealed class WebSocketConnectionActor : Actor, IWebSocketConnectionActor, IActor
Inheritance
WebSocketConnectionActor
Implements
Inherited Members

Constructors

WebSocketConnectionActor(ActorActivationContext, IWebSocketService)

One actor instance per WebSocket connection, keyed by connection id. Dapr's placement service guarantees at most one live activation of a given ActorId cluster-wide -- that guarantee, not a hand-rolled InstanceId directory + heartbeat + forward-or-reclaim pattern (Virtufin.Service.MultiInstance, removed by this migration -- see its Virtufin.Service 0.1.0 CHANGELOG entry), is what now ensures every RPC for a connection reaches the one process that actually holds its live ClientWebSocket.

A thin router, not a state owner: every method delegates straight to the pre-existing IWebSocketService, which still does the real work (open/close the socket, persist metadata via IDaprConnectionRepository) exactly as before. This mirrors the one part of the original (classic Dapr.Actors) 2026 attempt that was never the problem -- reusing existing business logic rather than duplicating it in the actor. What that attempt got wrong, and this one fixes: ConnectAsync(ConnectPayload, CancellationToken) is called immediately after the connection id is minted (see WebSocketManagerGrpcService.Connect), so the very first activation -- wherever Dapr's placement puts it -- is also where the real socket gets opened. Nothing here ever assumes "the socket is already open somewhere else."

public WebSocketConnectionActor(ActorActivationContext context, IWebSocketService webSocketService)

Parameters

context ActorActivationContext
webSocketService IWebSocketService

Properties

Id

Gets the current actor id.

protected override ActorId Id { get; }

Property Value

ActorId

State

Gets the actor state accessor for the current activation.

protected override IActorStateAccessor State { get; }

Property Value

IActorStateAccessor

Methods

ConnectAsync(ConnectPayload, CancellationToken)

public Task<WebSocketConnectionSnapshot> ConnectAsync(ConnectPayload body, CancellationToken cancellationToken = default)

Parameters

body ConnectPayload
cancellationToken CancellationToken

Returns

Task<WebSocketConnectionSnapshot>

DisconnectAsync(CancellationToken)

public Task DisconnectAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task

GetTagAsync(string, CancellationToken)

public Task<string?> GetTagAsync(string key, CancellationToken cancellationToken = default)

Parameters

key string
cancellationToken CancellationToken

Returns

Task<string>

GetTagsAsync(CancellationToken)

public Task<TagsPayload> GetTagsAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task<TagsPayload>

OnActivateAsync(CancellationToken)

Runs before any method call reaches this activation. A fresh ConnectAsync(ConnectPayload, CancellationToken) registers the connection locally itself, so this is a no-op for that (normal) case; it only does real work when Dapr has activated this connection's actor on an instance that never opened its socket -- a genuine failover/rebalance of an already-connected id -- closing the gap the 2026 classic-Dapr.Actors attempt left open (see the class doc comment above).

protected override ValueTask OnActivateAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

Returns

ValueTask

SendAsync(SendPayload, CancellationToken)

public Task<byte[]> SendAsync(SendPayload body, CancellationToken cancellationToken = default)

Parameters

body SendPayload
cancellationToken CancellationToken

Returns

Task<byte[]>

SendRawAsync(SendRawPayload, CancellationToken)

public Task SendRawAsync(SendRawPayload body, CancellationToken cancellationToken = default)

Parameters

body SendRawPayload
cancellationToken CancellationToken

Returns

Task

SetTagAsync(SetTagPayload, CancellationToken)

public Task SetTagAsync(SetTagPayload body, CancellationToken cancellationToken = default)

Parameters

body SetTagPayload
cancellationToken CancellationToken

Returns

Task

SetTagsAsync(Dictionary<string, string>, CancellationToken)

public Task SetTagsAsync(Dictionary<string, string> tags, CancellationToken cancellationToken = default)

Parameters

tags Dictionary<string, string>
cancellationToken CancellationToken

Returns

Task

StartPublishAsync(string, CancellationToken)

public Task StartPublishAsync(string topic, CancellationToken cancellationToken = default)

Parameters

topic string
cancellationToken CancellationToken

Returns

Task

StopPublishAsync(CancellationToken)

public Task StopPublishAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task

TouchAsync(CancellationToken)

No-op invocation whose only purpose is resetting Dapr's per-actor idle timer. A connection that only ever receives (the common case: subscribe once, then relay inbound messages to pub/sub) never calls any other actor method after ConnectAsync(ConnectPayload, CancellationToken) -- without this, ConnectionHeartbeatService's periodic call here is what keeps a long-lived, quiet connection's actor from being idle-deactivated (and, on a later invocation, reactivated on a different node with no real socket) while the real socket is still open. See the app-wide ActorIdleTimeout in Program.cs for the backstop this is meant to make practically unreachable, not relied on.

public Task TouchAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task