Repository navigation
Serialization
ArgusTransfer handles two layers of serialization: wire serialization (the ARGUS/1.0 protocol format) and body serialization (the message payload).
The ArgusRequestSerializer and ArgusResponseSerializer handle reading and writing the full ARGUS/1.0 wire format. These are used internally by the client and server -- you do not need to interact with them directly in most cases.
Each serializer provides:
- A byte-safe
StreamAPI, used by the client and the pipe host:WriteAsync(Stream, message)/ReadAsync(Stream)(plusWriteAsync(Stream, response, acceptContentType)on the response serializer). Bodies are written and read as raw bytes,Content-Lengthand chunk sizes are byte counts, and compressed bodies are supported. - A synchronous string API for testing and in-memory use:
Write(message)/Read(string text) -
StreamWriter/StreamReaderoverloads, kept for compatibility:WriteAsync(StreamWriter, message)flushes the writer and writes bytes to itsBaseStream(byte-safe);ReadAsync(StreamReader)andRead(string)countContent-Lengthin UTF-8 bytes.
Use the Stream overloads for transport code. The text-based paths (string, StreamReader, synchronous StreamWriter) decode chunked data as UTF-8, so they only round-trip streamed bodies that are valid UTF-8 text, and they throw InvalidOperationException for a message with a Content-Encoding.
| Situation |
Stream overloads |
Text-based overloads |
|---|---|---|
| Non-ASCII string body | ✓ | ✓ |
| Binary streamed body | ✓ | corrupted (non-UTF-8 bytes) |
Content-Encoding (compression) |
✓ | InvalidOperationException |
Body shorter than Content-Length
|
EndOfStreamException |
truncated body returned |
The serializers' ContentEncodings property lists the encodings used to compress and decompress bodies on the Stream overloads (gzip by default). See Compression.
Body serializers handle the conversion of the message body for a specific content type. They implement the IArgusBodySerializer interface:
public interface IArgusBodySerializer
{
string ContentType { get; }
string WriteBody(string body);
string ReadBody(string body);
}| Member | Description |
|---|---|
ContentType |
The MIME type this serializer handles (e.g., "text/plain", "application/json") |
WriteBody |
Transforms the body before writing to the wire |
ReadBody |
Transforms the body after reading from the wire |
ArgusTransfer ships with a PlainTextArgusBodySerializer that handles text/plain content. It performs a pass-through -- the body is written and read as-is without transformation.
This serializer is registered automatically when you call AddArgusPipeHost() or AddArgusPlainTextProtocol().
The IArgusBodySerializerRegistry resolves serializers by content type:
public interface IArgusBodySerializerRegistry
{
IArgusBodySerializer DefaultSerializer { get; }
bool TryGetSerializer(string contentType, out IArgusBodySerializer serializer);
IReadOnlyCollection<IArgusBodySerializer> GetSerializers() => [this.DefaultSerializer];
}-
DefaultSerializer-- the fallback serializer when no content type is specified -
TryGetSerializer-- looks up a serializer matching the given content type -
GetSerializers-- all registered serializers, used to match wildcard media ranges such asapplication/*in anAcceptheader. It has a default implementation, so existing custom registries keep compiling; without an override, wildcards only match the default serializer (exact media types still work throughTryGetSerializer).ArgusBodySerializerRegistryreturns every registered serializer.
The server uses the registry to:
- Select the appropriate serializer for reading request bodies based on the
Content-Typeheader - Negotiate the response serializer from the
Acceptheader -- returning 406 NotAcceptable only if no registered serializer is acceptable (see Content Negotiation Flow)
Register the default plain text protocol:
services.AddArgusPlainTextProtocol();This is called automatically by AddArgusPipeHost(), so you typically do not need to call it explicitly.
Add a custom body serializer (for example the JsonArgusBodySerializer written in the next section -- the library itself ships only PlainTextArgusBodySerializer):
services.AddArgusBodySerializer<JsonArgusBodySerializer>();Serializers are deduplicated -- registering the same type twice has no effect.
To support a custom content type, implement IArgusBodySerializer:
public class JsonArgusBodySerializer : IArgusBodySerializer
{
public string ContentType => "application/json";
public string WriteBody(string body)
{
// Transform body before writing to wire (if needed)
return body;
}
public string ReadBody(string body)
{
// Transform body after reading from wire (if needed)
return body;
}
}Register it during service configuration:
services.AddArgusBodySerializer<JsonArgusBodySerializer>();For large or streaming payloads, ArgusTransfer supports chunked transfer encoding via the ArgusChunkedEncoding class. Chunked encoding is used automatically when a message has a BodyStream set instead of a Body string.
Chunked format:
{hex-chunk-size}\r\n
{chunk-data}\r\n
...
0\r\n
\r\n
On the client side, use the stream overloads of PostAsync, PutAsync, or PatchAsync to send chunked bodies:
using var stream = File.OpenRead("data.bin");
var response = await client.PostAsync("/upload", stream, "application/octet-stream");The server enforces MaxRequestBodySize on chunked bodies to prevent denial-of-service via unbounded streams.
For machine-readable error responses, ArgusProblemDetails (an RFC 7807 model under ArgusTransfer.Protocol) serializes itself to a JSON string with System.Text.Json and sets Content-Type: application/problem+json on the response. Because body serializers transform strings, the default PlainTextArgusBodySerializer passes that JSON through unchanged -- no extra serializer registration is needed:
context.Response = ArgusProblemDetails.BadRequest("name is required");
// or, with the correlation token as the problem's instance:
context.Problem(ArgusStatusCode.BadRequest, "name is required");See Error Handling for the full property model, the factory methods, client-side parsing with TryRead, and how Extensions are serialized.
When a request arrives at the server:
- If the request has a
Content-Typeheader, the registry resolves the appropriate serializer for reading the body - If the request has an
Acceptheader, the server negotiates the response serializer from it -- if no registered serializer is acceptable, the server returns 406 NotAcceptable - If no
Acceptheader is present (the default for a newArgusRequest), any media type is acceptable and the default serializer is used - The response is written with the negotiated serializer, whose content type becomes the response's
Content-Type. A handler that setsContent-Typeitself keeps it, and the body is written with the serializer registered for that content type
Accept is a comma-separated list of media ranges, each with an optional quality value, as in HTTP:
Accept: application/json, text/plain;q=0.5, */*;q=0.1
- A range is
type/subtype,type/*or*/*(*alone is read as*/*). Matching is case-insensitive, and parameters other thanq(such ascharset) are ignored. -
qis a number from 0 to 1 (default 1).q=0means "not acceptable" and excludes the matching serializers. - A serializer's quality comes from the most specific range that matches its content type, so
*/*, text/plain;q=0accepts everything except plain text. - The acceptable serializer with the highest quality wins. Ties go to the more specific range, then to the range listed first, then to the default serializer.
- Malformed ranges (no
/, an invalidq) are skipped. A header without any valid range is treated as no preference.
Accept (registered: text/plain default, application/json) |
Response |
|---|---|
| (none) | text/plain |
application/json, text/plain |
application/json |
text/plain;q=0.5, application/json |
application/json |
application/* |
application/json |
*/* |
text/plain (the default) |
application/xml |
406 NotAcceptable |
application/json;q=0 |
406 NotAcceptable |
The same rules apply when you call ArgusResponseSerializer.Write(response, accept) or WriteAsync(stream, response, accept, …) with a registry: the accept argument may be a full Accept header value. If nothing is acceptable, these methods fall back to the default serializer; only the host answers 406.
In earlier versions,
ArgusRequestalways sentAccept: text/plainand the host matched the whole header value literally, so lists and quality values always produced 406. Code that relied onrequest.Acceptbeing"text/plain"by default now seesnull.
copyright @ Sam Gerené