Core concepts
This page describes the vocabulary and shape of Bodu.Text.Bencode. Its sibling serializers - Bodu.Text.Toml and Bodu.Text.Yaml - share the same architecture, so what you learn here transfers with the prefix changed; see the family introduction for the cross-library view.
Part of the Text & Serialization topic.
The serializer
The static BencodeSerializer is the high-level entry point. Serialize<T> writes an object graph to Bencode; Deserialize<T> binds Bencode back to a type. Each has overloads over the format's natural surfaces:
| Direction | Overloads |
|---|---|
Serialize<T> |
to byte[] (the return value), IBufferWriter<byte>, and Stream, plus SerializeAsync<T>(Stream, …). |
Deserialize<T> |
from ReadOnlySpan<byte>, byte[], and Stream, plus DeserializeAsync<T>(Stream, …). |
| DOM bridges | SerializeToNode<T> (to a mutable BencodeNode), SerializeToDocument<T> (to a read-only BencodeDocument), and Deserialize<T>(BencodeNode, …) (bind straight from a node tree). |
Every overload accepts an optional BencodeSerializerOptions; the async pair takes it before the CancellationToken. There are no TryDeserialize-style members - wrap a call in a try/catch over the two exception types when reading untrusted input.
Options
BencodeSerializerOptions configures the serializer:
| Member | Default | Governs |
|---|---|---|
Converters |
empty | The user converter list, searched ahead of the built-ins. |
PropertyNamingPolicy |
null |
The NamingPolicy applied to member names with no explicit [PropertyName]. |
PropertyNameCaseInsensitive |
false |
Whether a document key matches a member name ignoring case on read. |
IncludeFields |
false |
Whether public fields join properties as serializable members. |
DefaultIgnoreCondition |
Never |
The fallback IgnoreCondition for members with no explicit [Ignore]. |
UnmappedMemberHandling |
Skip |
Whether an unmapped key is skipped or rejected on read (UnmappedMemberHandling). |
PreferredObjectCreationHandling |
Replace |
Whether a member is replaced or populated on read (ObjectCreationHandling). |
AllowUnsortedKeys |
false |
Read-only leniency: accept dictionaries whose keys are not in ascending bytewise order. |
AllowDuplicateKeys |
false |
Read-only leniency: accept repeated keys (last occurrence wins). |
MaxDepth |
64 (DefaultMaxDepth) |
The maximum nesting depth before a depth guard trips. |
Construct one from a BencodeSerializerDefaults value to start from a scenario's conventions: General leaves names unchanged with default-case matching; Web applies camel-case naming and case-insensitive matching.
AllowUnsortedKeys and AllowDuplicateKeys relax only the read path - the writer is unconditionally canonical, so anything written is byte-for-byte BEP 3 regardless of how lenient the read was.
An options instance becomes read-only the first time it is used - or eagerly via MakeReadOnly() (IsReadOnly reports the state) - and then caches its resolved converters and type metadata. Mutating a frozen instance throws InvalidOperationException. Configure one options object and reuse it across many operations.
Converters and resolution
A BencodeConverter<T> converts one type, reading through the Utf8BencodeReader and writing through the Utf8BencodeWriter. A BencodeConverterFactory produces converters for a family of types (every Nullable<T>, every enum, every collection) - the same pattern the built-in converters use.
For a given type the serializer resolves a converter by checking, in order:
- a member-level converter attribute (
[Converter(typeof(…))]); - a type-level converter attribute;
- the first matching converter in
options.Converters; - the built-in converters.
The first match wins, and the result is cached on the options.
Attributes, callbacks, and naming policies
The full serialization surface lives in the Bodu.Text.Bencode.Serialization namespace:
- Attributes -
[PropertyName],[Ignore],[Converter],[PropertyOrder],[Constructor],[Required],[Include],[ExtensionData],[NamingPolicy],[UnmappedMemberHandling],[ObjectCreationHandling],[StringEnumMemberName]. - Callbacks - the IOnSerializing / IOnSerialized / IOnDeserializing / IOnDeserialized interfaces, run at the matching point in the pipeline.
- Naming policies - NamingPolicy
.CamelCase,.SnakeCaseLower/.SnakeCaseUpper,.KebabCaseLower/.KebabCaseUpper, plus theBencodeSerializerDefaults.Webpreset. - Enum converters - a string-enum converter (member names) and a number-enum converter.
The document object models
When you do not want a model, the library offers two DOMs:
- Mutable - BencodeNode with the concrete
BencodeObject(a keyed dictionary node),BencodeArray(a list node), andBencodeValue(a scalar node).Parsea document into a tree, index into it withnode["key"]/node[index], mutate it, and write it back withToByteArray(). Scalars convert with implicit operators (string,long,int,ulong,byte[]→BencodeNode) and explicit operators back the other way, and the tree supportsDeepClone(),DeepEquals(…),ReplaceWith(…), andGetPath().Parsereturnsnullfor an empty document. - Read-only - BencodeDocument with
BencodeElementandBencodeProperty. A low-allocation view over a parsed buffer, walked throughRootElement:GetProperty/TryGetProperty, the integer indexer for lists,EnumerateObject()/EnumerateArray(), and typed getters (GetString,GetBytes,GetInt64,GetUInt64, and theTryGet…pair). Each element's kind is a BencodeValueKind (Object,Array,ByteString,Integer).BencodeDocumentis disposable - it owns a pooled buffer, so wrap it inusingandClone()out any element that must outlive it.
The low-level reader and writer
Utf8BencodeReader and Utf8BencodeWriter are forward-only, allocation-free ref struct token machines over ReadOnlySpan<byte> and IBufferWriter<byte> respectively. The serializer and every converter are built on this pair; reach for it directly to process tokens without binding to a model.
The reader is positioned on a token by Read() (which returns false at the end), and the token is classified by TokenType - a BencodeTokenType with the values None, StartList, EndList, StartDictionary, EndDictionary, PropertyName, Integer, and ByteString. A Bencode dictionary surfaces as alternating PropertyName and value tokens between StartDictionary and EndDictionary. Once positioned, value getters read the current token without advancing:
| Reader member | Reads |
|---|---|
GetString() / GetBytes() |
The current byte-string or property-name token as UTF-8 text or raw bytes. |
GetInt32() / GetInt64() / GetUInt64() |
The current integer token, range-checked to the target width; the TryGet… overloads return false instead of throwing. |
ValueSpan / ValueTextEquals(…) |
The raw token bytes, or a zero-allocation comparison against UTF-8/char/string text. |
Skip() / TrySkip() |
Step over the current value in full, including a nested list or dictionary subtree. |
BytesConsumed / CurrentDepth / TokenStartIndex |
Diagnostic position state. |
The writer is the dual: structural pairs WriteStartList() / WriteEndList() and WriteStartDictionary() / WriteEndDictionary(), dictionary keys via WritePropertyName(…), and scalars via WriteInteger(long) / WriteInteger(ulong), WriteByteString(ReadOnlySpan<byte>), and WriteString(string) (UTF-8). Convenience name-plus-value overloads (for example WriteString(name, value)) write a key and its value in one call, and WriteRawValue(…) splices a pre-encoded fragment. The writer re-sorts each dictionary's entries into ascending bytewise key order when the dictionary closes, so canonical output is automatic regardless of the order keys are presented.
Value mapping
Bencode maps the BCL types it can represent natively and rejects the rest unless a converter handles them.
string, byte[], and memory-of-byte map to byte strings; the integer family (through ulong.MaxValue, including the 128-bit types within the 64-bit surfaces) maps to i…e; enums map to member-name byte strings; collections (including queues, stacks, and the concurrent collections) map to lists; objects and dictionaries map to dictionaries with keys in canonical ascending bytewise order. Dictionary keys may be strings, integers, enums, Guid, bool, or char, stringified on the wire. Booleans, floating-point, and date-times have no Bencode form and require a registered converter. An object-typed member writes its runtime type and reads back as a BencodeElement. A null member is omitted on write; public fields participate via IncludeFields or [Include].
Errors
A malformed document raises BencodeFormatException (carrying the byte Offset where parsing failed) - truncated data, non-canonical integers, trailing bytes, and, unless the matching leniency option is set, out-of-order or duplicate dictionary keys. A document that parses but cannot bind to your type - a type mismatch, a missing required member, a value out of range for the target - raises BencodeSerializationException (carrying the byte BytesOffset of the failing value where one is known).
Where to go next
- Bodu.Text.Bencode introduction - what is specific to the format: byte strings, canonical output, the kinds it cannot represent.
- Getting started - install and the first round trip.
- Using Bencode - the worked walk-through.
- Bodu serializers introduction - the shared family shape.
- Text & Serialization topic overview - where the serializers sit among the codecs and document formats.