Mapping attributes
The Bencode serializer honours the attribute family from the shared Bodu.Text.Serialization package for shaping how a type maps to the wire - every one derives SerializationAttribute. The sibling TOML and YAML serializers honour the very same attributes (there is no per-format prefix); the patterns transfer directly. Each pattern below shows the Bencode form and the dictionary entry it writes.
Pattern 1 - Rename a member
PropertyNameAttribute pins the serialized key for one member, beating any naming policy:
public sealed class Profile
{
[PropertyName("display-name")]
public string DisplayName { get; set; } = "Ada";
}
This writes the dictionary entry 12:display-name3:Ada.
Pattern 2 - Apply a naming policy per type
NamingPolicyAttribute overrides the options-level PropertyNamingPolicy for one type:
[NamingPolicy(KnownNamingPolicy.SnakeCaseLower)]
public sealed class RetryPolicy
{
public int MaxRetryCount { get; set; } = 5;
}
// → d15:max_retry_counti5ee
A member carrying [PropertyName] is unaffected - the explicit name always wins. KnownNamingPolicy.Unspecified defers back to the options.
Pattern 3 - Exclude members
IgnoreAttribute drops a member unconditionally, or under a condition:
public sealed class Account
{
public string Name { get; set; } = "svc";
[Ignore]
public string? Secret { get; set; }
[Ignore(Condition = IgnoreCondition.WhenWritingNull)]
public string? Comment { get; set; }
}
// → d4:Name3:svce
Secret is never written; Comment appears only when non-null. WhenWritingDefault extends the rule to default value-type values.
Pattern 4 - Force a member in
IncludeAttribute binds non-public property accessors and surfaces public fields without turning on IncludeFields for the whole options:
public sealed class Counter
{
[Include]
public int Total { get; private set; }
[Include]
public int Retries;
}
Both members now round-trip - the private setter is assigned on read, and the field participates like a property, following the same naming, ordering, ignore, required, and converter rules.
Pattern 5 - Control write order
PropertyOrderAttribute reorders the order members are presented to the writer; members without the attribute default to order zero and keep declaration order:
public sealed class Manifest
{
[PropertyOrder(2)]
public string Name { get; set; } = "demo";
[PropertyOrder(1)]
public int Version { get; set; } = 3;
}
Note
The attribute governs only the order members are presented to the writer - the writer re-sorts dictionary entries into canonical ascending key order when the dictionary closes, so the example above still emits d4:Name4:demo7:Versioni3ee regardless of [PropertyOrder]. The attribute matters when a custom converter walks members in presentation order.
Pattern 6 - Require a key
RequiredAttribute makes deserialization fail when the key is absent, with the same effect as declaring the member with the C# required keyword:
public sealed class ServerConfig
{
[Required]
public string Host { get; set; } = string.Empty;
}
// Input without a "Host" key throws BencodeSerializationException.
Pattern 7 - Pick the deserialization constructor
When a type declares more than one constructor, ConstructorAttribute resolves the ambiguity:
public sealed class Endpoint
{
[Constructor]
public Endpoint(string host, int port) => (Host, Port) = (host, port);
public Endpoint(Uri uri) : this(uri.Host, uri.Port) { }
public string Host { get; }
public int Port { get; }
}
Without the attribute the serializer resolves the constructor in this order: a constructor carrying [Constructor], then a public parameterless constructor (or, for a value type, default construction), then the public constructor with the most parameters. The chosen constructor's parameters are bound to members by matching parameter name to member name case-insensitively, so a host/port constructor binds the Host/Port members above.
Pattern 8 - Capture unknown keys
ExtensionDataAttribute designates one member that collects every key that maps to no other member, and writes the collected entries back out on serialization:
public sealed class ServerConfig
{
public int Port { get; set; }
[ExtensionData]
public Dictionary<string, BencodeNode>? Extra { get; set; }
}
The member must be a BencodeObject, an IDictionary<string, BencodeNode>, or a Dictionary<string, BencodeNode>, and a type may declare at most one. Because the writer re-sorts dictionary entries on close, the captured extension keys merge into the correct canonical position alongside the mapped members on the way back out.
Pattern 9 - Reject unknown keys
UnmappedMemberHandlingAttribute chooses, per type, between skipping a key that maps to no member (the default) and failing:
[UnmappedMemberHandling(UnmappedMemberHandling.Disallow)]
public sealed class StrictConfig
{
public int Port { get; set; }
}
// Input containing an unrecognised key throws BencodeSerializationException.
A type with an extension-data member (Pattern 8) still captures unmapped keys into that member regardless of this setting.
Pattern 10 - Populate instead of replace
ObjectCreationHandlingAttribute controls whether deserialization replaces a member's value with a fresh instance or populates the instance already held - useful for get-only collection properties:
public sealed class Pipeline
{
[ObjectCreationHandling(ObjectCreationHandling.Populate)]
public List<string> Steps { get; } = new() { "restore" };
}
Deserialized entries are appended to the existing list instead of replacing it. The attribute applies to a member or a whole type; member beats type, and both beat the options-level PreferredObjectCreationHandling.
Pattern 11 - Choose a converter
ConverterAttribute selects the converter for a member, or for every use of a type:
public sealed class Stamped
{
[Converter(typeof(UnixSecondsConverter))]
public DateTimeOffset CreatedAt { get; set; }
}
// → d9:CreatedAti1700000000ee
The referenced type must derive from BencodeConverter<T> (or its factory) and expose a public parameterless constructor. Writing converters - including the factory pattern and resolution order - is covered in Writing converters.
Pattern 12 - Name enum members
StringEnumMemberNameAttribute renames an individual enumeration member when the enum is serialized by name:
[Converter(typeof(BencodeStringEnumConverter<Status>))]
public enum Status
{
Active,
[StringEnumMemberName("on-hold")]
OnHold,
}
// Status.OnHold serializes as the byte string 7:on-hold
The attribute applies only to by-name serialization (the default enum handling, or an explicit string-enum converter); it is a no-op when the enum is written as an integer.
Precedence at a glance
When several settings could govern the same member, the closest one wins:
- a member-level attribute (
[PropertyName],[Ignore],[Converter],[ObjectCreationHandling], …); - a type-level attribute (
[NamingPolicy],[Converter],[UnmappedMemberHandling],[ObjectCreationHandling]); - the serializer options (
PropertyNamingPolicy,PropertyNameCaseInsensitive,Converters,DefaultIgnoreCondition,UnmappedMemberHandling,PreferredObjectCreationHandling,IncludeFields).
A member with no explicit [Ignore] falls back to the options-level DefaultIgnoreCondition (default Never); an absent [NamingPolicy] falls back to PropertyNamingPolicy. The [PropertyName] name always wins over every policy at any level.
See also
- Using Bencode - the format walk-through these attributes shape.
- Writing converters -
[Converter]placement and the resolution order in detail. - Bencode guides - the full guide index for the Bencode serializer.
- Text & Serialization guides - every guide in this topic, across Bodu.Text.Encoding, Bodu.Text.Formats, and the serializers.