Preserve DatabaseEntry MessagePack key slots

This commit is contained in:
Meta Vens 2026-05-06 04:56:42 +01:00
commit c627200e4a
2 changed files with 59 additions and 33 deletions

View file

@ -119,18 +119,24 @@ namespace GameCult.Caching.MessagePack.Generator
sb.AppendLine();
var members = CollectMembers(type);
var ordered = OrderMembers(type, members);
var slots = AssignSlots(type, members);
var maxSlot = slots.Count == 0 ? -1 : slots.Max(m => m.slot);
// Serialize
sb.AppendLine($" public void Serialize(ref MessagePackWriter writer, {typeFull}? value, MessagePackSerializerOptions options)");
sb.AppendLine(" {");
sb.AppendLine(" if (value == null) { writer.WriteNil(); return; }");
sb.AppendLine($" writer.WriteArrayHeader({ordered.Count});");
for (int i = 0; i < ordered.Count; i++)
sb.AppendLine($" writer.WriteArrayHeader({maxSlot + 1});");
for (int i = 0; i <= maxSlot; i++)
{
var (name, tsym) = ordered[i];
var tname = tsym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var access = $"value.{name}";
var member = slots.FirstOrDefault(m => m.slot == i);
if (member.name == null)
{
sb.AppendLine(" writer.WriteNil();");
continue;
}
var tname = member.typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var access = $"value.{member.name}";
sb.AppendLine(
tname == "global::System.Guid"
? $" options.Resolver.GetFormatterWithVerify<System.Guid>().Serialize(ref writer, {access}, options);"
@ -144,21 +150,26 @@ namespace GameCult.Caching.MessagePack.Generator
sb.AppendLine(" {");
sb.AppendLine(" if (reader.TryReadNil()) return null;");
sb.AppendLine($" var count = reader.ReadArrayHeader();");
sb.AppendLine($" if (count != {ordered.Count}) throw new MessagePackSerializationException($\"Invalid {type.Name} length {{count}}, expected {ordered.Count}\");");
for (int i = 0; i < ordered.Count; i++)
sb.AppendLine($" var obj = new {typeFull}();");
sb.AppendLine(" for (var i = 0; i < count; i++)");
sb.AppendLine(" {");
sb.AppendLine(" switch (i)");
sb.AppendLine(" {");
foreach (var (slot, name, tsym) in slots)
{
var (_, tsym) = ordered[i];
var tname = tsym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.AppendLine($" case {slot}:");
sb.AppendLine(
tname == "global::System.Guid"
? $" var v{i} = options.Resolver.GetFormatterWithVerify<System.Guid>().Deserialize(ref reader, options);"
: $" var v{i} = options.Resolver.GetFormatterWithVerify<{tname}>().Deserialize(ref reader, options);");
}
sb.AppendLine($" var obj = new {typeFull}();");
for (int i = 0; i < ordered.Count; i++)
{
sb.AppendLine($" obj.{ordered[i].name} = v{i};");
? $" obj.{name} = options.Resolver.GetFormatterWithVerify<System.Guid>().Deserialize(ref reader, options);"
: $" obj.{name} = options.Resolver.GetFormatterWithVerify<{tname}>().Deserialize(ref reader, options);");
sb.AppendLine(" break;");
}
sb.AppendLine(" default:");
sb.AppendLine(" reader.Skip();");
sb.AppendLine(" break;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" return obj;");
sb.AppendLine(" }");
@ -190,29 +201,34 @@ namespace GameCult.Caching.MessagePack.Generator
return members;
}
private static List<(string name, ITypeSymbol typeSymbol)> OrderMembers(
private static List<(int slot, string name, ITypeSymbol typeSymbol)> AssignSlots(
INamedTypeSymbol type,
List<(string name, ITypeSymbol typeSymbol, int? key, bool ignore)> members)
{
members = members.Where(m => !m.ignore).ToList();
var ordered = new List<(string, ITypeSymbol)>();
var slots = new List<(int, string, ITypeSymbol)>();
var id = FindIdMember(type);
if (id != null)
{
ordered.Add((id.Value.name, id.Value.typeSymbol));
slots.Add((0, id.Value.name, id.Value.typeSymbol));
members.RemoveAll(m => m.name == id.Value.name);
}
var keyed = members.Where(m => m.key.HasValue).OrderBy(m => m.key.Value)
.Select(m => (m.name, m.typeSymbol)).ToList();
var nonKeyed = members.Where(m => !m.key.HasValue)
.Select(m => (m.name, m.typeSymbol)).ToList();
foreach (var member in members.Where(m => m.key.HasValue).OrderBy(m => m.key!.Value))
{
slots.Add((member.key!.Value, member.name, member.typeSymbol));
}
ordered.AddRange(keyed);
ordered.AddRange(nonKeyed);
return ordered;
var nextSlot = slots.Count == 0 ? 0 : slots.Max(s => s.Item1) + 1;
foreach (var member in members.Where(m => !m.key.HasValue))
{
slots.Add((nextSlot, member.name, member.typeSymbol));
nextSlot++;
}
return slots.OrderBy(s => s.Item1).ToList();
}
private static int? GetKeyValue(ISymbol sym)

View file

@ -19,13 +19,23 @@ For each discovered concrete `DatabaseEntry` subtype, the generator emits:
- a formatter type
- a resolver mapping entry
Member ordering is:
Member slot assignment is:
1. `ID` first
2. members with `[Key(...)]`, ordered by key
3. remaining non-ignored instance fields and properties
1. `ID` in slot `0`
2. members with `[Key(...)]` in their exact key slot
3. remaining non-ignored instance fields and properties appended after the
highest occupied slot as legacy convenience
That gives predictable binary layout while still supporting manual `[Key]` ordering.
The generated formatter writes a MessagePack array sized to the highest occupied
slot and writes nil for unused gaps. Deserialization tolerates shorter older
arrays by leaving missing members at their constructor/field-initializer values,
and skips unknown extra slots. That makes `[Key(n)]` a durable wire contract:
deleted fields can leave their slot reserved and new fields can use new keys
without a store migration.
For schema-stable cache entries, annotate every persisted member. Unkeyed
members are supported for older code, but they are not a good long-term wire
contract because source member order is not a schema.
## Example