Summary
[CanBeNull] on a collection field is also applied to the collection's elements (and dictionary keys/values), because every collection rule passes the field's full attribute list down when it resolves the element rule.
Where
ListMigrationRule, HashSetMigrationRule, SortedSetMigrationRule, ArrayMigrationRule, DictionaryMigrationRule all call SerializableMigrationRulesEngine.GenerateSerializableProperty(compilation, "…Entry", elementType, 0, attributes, parentSymbol, null) with the collection field's attributes. Element rules that honour [CanBeNull] (RawSerializableMigrationRule, PrimitiveUOTypeMigrationRule, nested collection rules) then emit per-element null handling.
Observed
[CanBeNull]
[SerializableField(2)]
private List<SpawnerEntry> _entryList; // SpawnerEntry is [SerializationGenerator]
generates (Serialize):
if (_entryList != default)
{
writer.Write(true);
var _entryListCount = _entryList?.Count ?? 0;
writer.WriteEncodedInt(_entryListCount);
if (_entryListCount > 0)
{
foreach (var _entryListEntry in _entryList!)
{
if (_entryListEntry != default) { writer.Write(true); _entryListEntry.Serialize(writer); }
else { writer.Write(false); }
}
}
}
and the matching read path allocates _entryListEntry = default into the list when the per-element flag is false. The schema records the flag twice: ["@CanBeNull", "Server.Engines.Spawners.SpawnerEntry", "RawSerializableMigrationRule", "DeserializationRequiresParent", "@CanBeNull"].
Why it matters
- One extra byte per element on disk for every
[CanBeNull] collection whose element rule honours the flag.
- The read path admits
null elements into a list the author never declared as holding nulls.
- There is no way to say "the collection may be null, its elements may not".
Wire-format caveat
Any existing [CanBeNull] collection with raw-serializable elements already stores the per-element flag, so changing propagation is a wire-format change for those types. In ModernUO at least PuzzleChest._guesses (Dictionary<Mobile, PuzzleChestSolutionAndTime>) is affected; List<Mobile> / HashSet<Item> / List<Item> uses go through the entity rule and are unaffected. A fix therefore needs a version bump and MigrateFrom on those consumers, landed together with the generator release.
Proposed fix
Strip [CanBeNull] (and [Tidy], which is a collection concern) from the attributes handed to element/key/value rules. If nullable elements are ever wanted, add an explicit annotation for that ([CanBeNullElements] or similar) rather than inferring it from the collection's attribute. Add a snapshot fixture with a [CanBeNull] List<NestedSerializable> pinning the single-flag output, and bump the affected ModernUO types in the same release.
Found while working on modernuo/ModernUO#2621, which avoids the annotation for now.
Summary
[CanBeNull]on a collection field is also applied to the collection's elements (and dictionary keys/values), because every collection rule passes the field's full attribute list down when it resolves the element rule.Where
ListMigrationRule,HashSetMigrationRule,SortedSetMigrationRule,ArrayMigrationRule,DictionaryMigrationRuleall callSerializableMigrationRulesEngine.GenerateSerializableProperty(compilation, "…Entry", elementType, 0, attributes, parentSymbol, null)with the collection field'sattributes. Element rules that honour[CanBeNull](RawSerializableMigrationRule,PrimitiveUOTypeMigrationRule, nested collection rules) then emit per-element null handling.Observed
generates (Serialize):
and the matching read path allocates
_entryListEntry = defaultinto the list when the per-element flag is false. The schema records the flag twice:["@CanBeNull", "Server.Engines.Spawners.SpawnerEntry", "RawSerializableMigrationRule", "DeserializationRequiresParent", "@CanBeNull"].Why it matters
[CanBeNull]collection whose element rule honours the flag.nullelements into a list the author never declared as holding nulls.Wire-format caveat
Any existing
[CanBeNull]collection with raw-serializable elements already stores the per-element flag, so changing propagation is a wire-format change for those types. In ModernUO at leastPuzzleChest._guesses(Dictionary<Mobile, PuzzleChestSolutionAndTime>) is affected;List<Mobile>/HashSet<Item>/List<Item>uses go through the entity rule and are unaffected. A fix therefore needs a version bump andMigrateFromon those consumers, landed together with the generator release.Proposed fix
Strip
[CanBeNull](and[Tidy], which is a collection concern) from the attributes handed to element/key/value rules. If nullable elements are ever wanted, add an explicit annotation for that ([CanBeNullElements]or similar) rather than inferring it from the collection's attribute. Add a snapshot fixture with a[CanBeNull] List<NestedSerializable>pinning the single-flag output, and bump the affected ModernUO types in the same release.Found while working on modernuo/ModernUO#2621, which avoids the annotation for now.