2.52.0
(feat): Conditionally emit System.Text.Json as a PackageReference only when
targeting pre-net8.0 TFMs (netstandard2.0, net462). The package is
wrapped in an ItemGroup with an MSBuild IsTargetFrameworkCompatible
condition so it is skipped for net8.0+ where it ships in-box.
2.51.1
(chore): Remove dead #elif NET7_0_OR_GREATER and #else branches inside
QueryStringBuilder that could only execute on .NET 6 or .NET 7,
which are no longer targeted.
2.51.0
(feat): Add net9.0 target framework to all generated C# SDK projects. The library
now targets net462;net8.0;net9.0;netstandard2.0, enabling #if NET9_0_OR_GREATER
code paths such as ClientWebSocketOptions.KeepAliveTimeout for transport-level
dead connection detection. Test projects now target net9.0 instead of net8.0.
(feat): Add comprehensive WebSocket unit and end-to-end test templates. All generated
WebSocket SDKs now ship with 349 tests covering AsyncLock, Event<T>,
ReconnectStrategy, DisconnectionInfo, RequestMessage, Query,
WebSocketConnection, WebSocketClient, ReconnectionInfo, and
WebsocketException. End-to-end tests run against an in-process
TestWebSocketServer and cover echo, rapid-fire, reconnection, server close,
abort recovery, concurrent clients, large messages, and full lifecycle.
Templates are conditionally emitted when the Fern definition includes WebSocket
endpoints.
(feat): Expose StateCheckInterval property on WebSocketClient. The property
(default: 5 seconds) forwards to WebSocketConnection in ConnectAsync,
allowing callers to control how often the background state monitor polls
for silent disconnections.
(fix): Fix server-initiated close skipping reconnection. When the server sent a
WebSocketMessageType.Close frame, the Listen() method used return
which exited entirely — bypassing the ReconnectSynchronized call. Changed
to break with a closedByServer flag so reconnection executes with
ReconnectionType.ByServer.
(fix): Fix CancelReconnection flag being ignored for server-initiated close.
The OnDisconnectionHappened callback fires in the Close handler and the
user can set info.CancelReconnection = true, but this flag was never
checked before calling ReconnectSynchronized. Now captured into a local
cancelReconnect variable and checked before reconnecting.
2.50.0
(feat): Add exponential backoff reconnection strategy for WebSocket connections.
A new ReconnectStrategy class provides configurable exponential backoff
(doubling intervals from 1s to 60s), optional random jitter (0-25%) to
prevent thundering herd, and configurable max attempts. The strategy is
exposed as a ReconnectBackoff property on the generated Options class
and wired through WebSocketClient to WebSocketConnection.Reconnect().
When max attempts are exhausted, reconnection stops and a disconnection
event is raised. The backoff resets on successful reconnection.
2.49.0
(feat): Add per-send timeout to prevent SendAsync hangs. A new SendTimeout property
(default: 30 seconds) on WebSocketConnection wraps every SendAsync call with a
linked CancellationTokenSource that fires CancelAfter(SendTimeout). When the
timeout elapses the connection is aborted and a WebsocketException is thrown,
preventing a single dead connection from blocking the send lock for up to 30 minutes.
See: https://github.com/dotnet/runtime/issues/125257
2.48.0
(feat): Add bounded send queue with message expiration to WebSocketConnection. The
unbounded Channel<T> queues are replaced with configurable bounded channels
(SendQueueLimit, default: 10,000) that use BoundedChannelFullMode.DropWrite
to reject new messages when the queue is full. A SendCacheItemTimeout property
(default: 30 minutes) causes stale messages to be silently dropped during drain,
preventing memory growth during network outages and avoiding sending outdated
messages after reconnection. Set SendQueueLimit to 0 for unbounded or
SendCacheItemTimeout to null to disable expiration.
2.47.0
(feat): Add receive hang detection via a background state monitor task. A new
StateCheckInterval property (default: 5 seconds) controls how often the
monitor polls WebSocket.State. When the state transitions to Closed,
Aborted, or CloseReceived — indicating the TCP connection died without
notifying the receive loop — the monitor cancels the pending ReceiveAsync
via a linked CancellationTokenSource. This works around known .NET runtime
issues where ReceiveAsync hangs indefinitely on silent TCP disconnections
(dotnet/runtime#110496) and Abort() fails to cancel a pending receive
(dotnet/runtime#44272). Set StateCheckInterval to null to disable.
2.46.0
(feat): Wire up WebSocket reconnection logic. The WebSocketConnection template now
contains a complete reconnection implementation ported from the Marfusios library:
heartbeat timer (ReconnectTimeout), error-retry timer (ErrorReconnectTimeout),
lost-connection delay (LostReconnectTimeout), and Reconnect()/ReconnectOrFail()
public methods. The generated WebSocket API classes expose a Reconnecting event
and an IsReconnectionEnabled option (default: false) so callers can opt in to
automatic reconnection. All timeout properties are configurable with sensible defaults.
2.45.1
(fix): Fix sticky compression on reconnect. DeflateOptions is now explicitly reset to
null when EnableCompression is false, preventing a previous non-null value
from persisting across connection cycles.
2.45.0
(feat): Add InjectTestMessage internal method to generated WebSocket client classes.
This allows unit tests to simulate incoming WebSocket messages by injecting raw
JSON strings through the normal OnTextMessage pipeline without requiring a real
WebSocket connection. The method is marked internal so it is only accessible
via [InternalsVisibleTo] in test projects, keeping the public API surface clean.
2.44.0
(feat): Add queued sending via Channel<T> to the WebSocket connection layer. Two
unbounded channels (text and binary) are drained by dedicated background tasks
started alongside the connection. The non-blocking Send(string) and Send(byte[])
methods return bool indicating whether the message was queued, while the existing
SendInstant async-per-send path remains unchanged. Queues are completed in Dispose
before cancellation tokens are signalled. Drain loop errors are surfaced through the
ExceptionOccurred event.
2.43.0
(feat): Add per-message deflate compression support (RFC 7692) for WebSocket connections.
A new EnableCompression option on the generated WebSocket Options class enables
WebSocketDeflateOptions via ClientWebSocketOptions.DangerousDeflateOptions.
This can significantly reduce bandwidth for text-heavy (JSON) WebSocket APIs.
Warning: Do not use compression when sending data containing secrets
(CRIME/BREACH vulnerability).
See https://learn.microsoft.com/dotnet/api/system.net.websockets.clientwebsocketoptions.dangerousdeflateoptions
for details.
2.42.0
(feat): Add support for HTTP/2 WebSockets via SocketsHttpHandler. A new HttpInvoker
property on the WebSocket Options class allows passing an HttpMessageInvoker to
enable multiplexing multiple WebSocket streams over a single TCP connection. When
set, the connection factory configures HttpVersion.Version20 and uses the
ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) overload on .NET 7+.
On older targets the invoker is ignored and the standard connect path is used.
2.41.0
(feat): Add ConnectTimeout property to WebSocketConnection and WebSocketClient with a
default of 5 seconds. The connection factory call in StartClient now uses a linked
CancellationTokenSource with CancelAfter(ConnectTimeout), preventing indefinite
hangs when the server is unresponsive.
2.40.0
(feat): Configure PING/PONG keep-alive on ClientWebSocket. The default connection factory
now sets KeepAliveInterval (default: 30 s) and, on .NET 9+, KeepAliveTimeout
(default: 20 s) to enable the PING/PONG keep-alive strategy that detects unresponsive
servers at the transport level. Both values are exposed as public properties on
WebSocketConnection for caller customization.
2.38.2
(fix): Add using declaration to JsonDocument in WebSocket OnTextMessage handler.
JsonDocument implements IDisposable and holds pooled memory buffers; without
disposal, high message throughput causes memory pressure. The generated code now
emits using var json = await JsonSerializer.DeserializeAsync<JsonDocument>(stream);
so the document is disposed when the method returns.