Library / SDK
dotnet/WatsonTcp avatar
dotnet/WatsonTcp

WatsonTcp: TCP messaging for C# with framing, TLS and Native AOT support

WatsonTcp is the easiest way to build TCP-based clients and servers in C#.

676 stars129 forksC#MIT

At a glance

What is it?
WatsonTcp builds TCP clients and servers in C# with message-level framing, fast disconnect detection, SSL and TLS support, and telemetry that plugs into OpenTelemetry or Prometheus through the base class library. The current release adds Native AOT and trimming compatibility, with a strict list of metadata types that work without reflection.
Who is it for?
WatsonTcp fits a .NET team that needs a message-oriented TCP transport rather than a raw socket, that wants both ends under your control so the framing contract is respected, and that now has to publish for Native AOT. It does not fit a system where one end is a non-WatsonTcp peer that will not speak the framing, since that is the library's own documented boundary.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Framing is a contract, and both ends have to honour it

WatsonTcp is presented as a way to build TCP-based clients and servers in C# with integrated framing, reliable transmission and fast disconnect detection. The framing is the part to understand before anything else, because the README puts it in an IMPORTANT note. WatsonTcp provides framing to ensure message-level delivery, and that dictates that you must either use WatsonTcp for both the server and the client, or ensure that your client or server exchanges messages with the WatsonTcp node using WatsonTcp's framing. FRAMING.md in the repository is the reference for the message structure. In practice that means TCP is being used as a transport for discrete messages rather than as a byte stream, and that a peer which does not implement the same framing will not interoperate. That is a deliberate trade, since a stream library gives you more freedom and less convenience, and it is the reason the project also tells you when to choose something else.

The author names two alternatives and says when to use each

The README does something few projects do, which is to point you at its siblings and explain the differences. If you want a library that does not use framing but has a similar implementation, it points at SuperSimpleTcp. If you want a library that does not use framing and provides explicit control over how much data to read, it points at CavemanTcp. The distinction is not cosmetic. Framing means WatsonTcp decides where a message ends, which is what you want when messages are discrete, but it also means you cannot simply read a variable number of bytes and interpret them yourself, which is the case CavemanTcp is built for. The same author maintains all three, and the topic tags on the repository list ssl, ssl-support, tls, rpc, framing and messaging, which describes the space these libraries occupy. The practical value of the pointers is that you can pick the level of control you need without evaluating three unrelated projects.

Native AOT compatibility in v6.5.0, with the wire format unchanged

The most recent section of the README covers v6.5.0, and its headline is that WatsonTcp is now Native AOT and trimming compatible. The package is marked IsAotCompatible, and therefore IsTrimmable, for net8.0 and net10.0, builds with zero trim and AOT analyzer warnings, and is verified by publishing and running a native test binary. The mechanism is that all JSON the library places on the wire, meaning message headers and handshake messages, is serialized with System.Text.Json source generation instead of runtime reflection. The wire format is unchanged and remains fully interoperable with earlier WatsonTcp versions and with custom non-WatsonTcp endpoints, which is the reassuring part: the AOT work is an implementation change rather than a protocol break. JIT applications are unaffected, since types not covered by source generation still fall back to reflection exactly as before. Two behavioural fixes come with it. A header serialization failure, for example an unsupported metadata value, is now thrown to the caller of SendAsync or SendAndWaitAsync instead of disconnecting an otherwise healthy connection, and SSL certificate loading on macOS was fixed, which had previously failed with PlatformNotSupportedException around EphemeralKeySet.

Publishing for AOT needs nothing special beyond the publish command

For a project moving to Native AOT, the operational news is short, because the README says nothing special is required and to publish as usual:

bash
dotnet publish -c Release -r linux-x64 -p:PublishAot=true

The real work is on the application side, in deciding what your metadata values are. Under Native AOT, metadata values, which are Dictionary<string, object>, must be of a type that has source-generated JSON metadata. The README lists what works with no extra effort: the primitives, including string, bool, the integer and floating point types and char; common structs such as Guid, DateTime, DateTimeOffset, TimeSpan and Uri; arrays including byte[] as base64, string[], int[], long[], double[], bool[], Guid[] and object[]; collections including List of string, int, long and object, plus Dictionary<string, object> and Dictionary<string, string>; the JSON types JsonElement, JsonDocument, JsonNode, JsonObject and JsonArray; the WatsonTcp enums MessageStatus, DisconnectReason and TlsVersion; and null values. Anything else needs your own context.

Custom metadata types need a source-generated context on both ends

If you want to send your own types as metadata, the pattern is to declare a source-generated context and hand it to the serialization helper on both the server and the client:

csharp
using System.Text.Json.Serialization;

public class Order { public int Id { get; set; } public string Sku { get; set; } }

[JsonSerializable(typeof(Order))]
internal partial class MyJsonContext : JsonSerializerContext { }

server.SerializationHelper = new DefaultSerializationHelper(MyJsonContext.Default);
client.SerializationHelper = new DefaultSerializationHelper(MyJsonContext.Default);

Sending a metadata value whose type has no JSON metadata under Native AOT throws NotSupportedException, naming the type, from SendAsync or SendAndWaitAsync, and the connection remains usable. Received metadata values continue to arrive as JsonElement. There is also a way to test this before you publish, which is the part worth copying into your own pipeline. Setting enableReflectionFallback to false on the DefaultSerializationHelper reproduces the Native AOT serialization behaviour inside a normal JIT application or test, so unregistered metadata types surface before a publish rather than after:

csharp
client.SerializationHelper = new DefaultSerializationHelper(MyJsonContext.Default, enableReflectionFallback: false);

The ReflectionFallbackEnabled property reports whether reflection fallback is active, and it is always false in Native AOT and trimmed applications.

Telemetry through the base class library, with no backend dependency

The v6.4.0 section is about observability, and the design decision is the interesting part. WatsonTcp emits standardized, vendor-neutral telemetry using only the .NET base class library, so it can be observed by Radiant, the OpenTelemetry SDK, Prometheus or any compatible host with no dependency on any telemetry backend. Concretely, metrics are recorded into a System.Diagnostics.Metrics.Meter named WatsonTcp, and distributed tracing spans are started from a System.Diagnostics.ActivitySource with the same name, and both names are exposed as public constants. The reason to use the base class library rather than an OpenTelemetry dependency is straightforward: an instrumentation library that drags in a specific backend is a dependency every consumer has to take, and one that plugs into the platform's own meter and activity source lets the application decide where the data goes. The trade-off is that the library provides no exporter of its own, so if you want the data to appear somewhere you have to wire it up in the host application.

Test coverage, project status and the version numbers that do not line up

The v6.5.0 testing notes describe a new aot Touchstone suite with 55 positive and negative scenarios, covering byte-for-byte wire format equivalence with reflection output, every wire enum, every built-in metadata type, custom resolvers, raw non-WatsonTcp peers, end-to-end plain, SSL, stream, sync and handshake exchanges, and failure behaviour for unregistered types, running in the CLI, xUnit and NUnit hosts. A second addition, src/Test.Aot, is a Native AOT console host published as a native binary with trim and AOT warnings treated as errors, running 23 end-to-end scenarios with reflection disabled. The project is part of the .NET Foundation alongside the .NET Runtime, and it is MIT licensed. The one thing that will confuse you is the version list. The recent releases recorded for the repository are 2.0.0 from 2019-09-05 and 1.3.9 from 2019-07-04, while the README documents v6.5.0 and v6.4.0, and the last push was on 2026-10-06. The NuGet version and the GitHub release tags are on different tracks, so check the package version on NuGet rather than assuming the release list reflects what is current.

Editorial conclusion

WatsonTcp fits a .NET team that needs a message-oriented TCP transport rather than a raw socket, that wants both ends under your control so the framing contract is respected, and that now has to publish for Native AOT. It does not fit a system where one end is a non-WatsonTcp peer that will not speak the framing, since that is the library's own documented boundary. Before adopting it, read FRAMING.md to understand the wire format you are committing to, decide whether the metadata type list is acceptable for your payloads, and if you are publishing to Native AOT, switch off reflection fallback in a test build before you publish so unregistered types surface early.

Frequently asked questions

What does framing mean in WatsonTcp, and does my client have to use WatsonTcp?

Framing gives message-level delivery, and the README states you must either use WatsonTcp on both the server and the client, or make sure your side exchanges messages with the WatsonTcp node using WatsonTcp's framing. FRAMING.md is the reference for the message structure.

When should I use SuperSimpleTcp or CavemanTcp instead?

The README points at SuperSimpleTcp if you want a similar implementation without framing, and at CavemanTcp if you want no framing and explicit control over how much data to read. All three are maintained by the same author.

How do I send my own types as metadata under Native AOT?

Declare a source-generated JsonSerializerContext for your type and hand it to DefaultSerializationHelper on both the server and the client, as the README example shows. A metadata type with no JSON metadata throws NotSupportedException from SendAsync or SendAndWaitAsync, and the connection stays usable.

How can I check Native AOT readiness before publishing?

Set enableReflectionFallback to false when constructing the DefaultSerializationHelper, which reproduces the Native AOT serialization behaviour in a normal JIT build or test. The ReflectionFallbackEnabled property reports whether the fallback is active, and it is always false in Native AOT and trimmed applications.

Official sources

  1. dotnet/WatsonTcp on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dotnet-watsontcp.svg)](https://hysenlabs.com/projects/dotnet-watsontcp)

Was this useful?