Own serialization options by the document's assembly
The static UntrustedData options are gone. An assembly declares its formatter resolvers with [assembly: CultCacheFormatterResolver(typeof(R))]; CultDocumentMessagePackSerialization.OptionsFor(assembly) composes them ahead of CultDocumentResolver and StandardResolver, once per assembly, and hands assemblies that declare nothing the base Options. Generated codecs and the reflective SerializeUntyped/DeserializeUntyped path both read OptionsFor(typeof(X).Assembly). No registration call, no mutable options, no load order. CultMessagePackSecurity copies UntrustedData and supplies a collision-resistant comparer over CultRecordRef<T>.Key.Value, so ref-keyed dictionaries deserialize. Tests: RefKeyedDictionaryRoundTrips failed on825b4f7("No hash-resistant equality comparer available"); DeclaredResolverEncodesValueTypeAsPositionalArray and UnknownAssemblyGetsBaseOptions did not compile there (new API); InteropNoteBytesUnchanged is a pin whose payload constant was captured on825b4f7. BenchItem, the one unkeyed [CultDocument] in any consumer (in-memory benchmark, no store), gains [Key]s in its existing slot order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6fcc2a7704
commit
ea5a07b37a
7 changed files with 270 additions and 10 deletions
|
|
@ -287,7 +287,7 @@ public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator
|
|||
builder.AppendLine($" private static byte[] {document.PayloadSerializerMethodName}(object document)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine($" var typed = ({document.DocumentTypeName})document;");
|
||||
builder.AppendLine(" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.Options;");
|
||||
builder.AppendLine($" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.OptionsFor(typeof({document.DocumentTypeName}).Assembly);");
|
||||
builder.AppendLine(" var buffer = new global::System.Buffers.ArrayBufferWriter<byte>();");
|
||||
builder.AppendLine(" var writer = new global::MessagePack.MessagePackWriter(buffer);");
|
||||
builder.AppendLine($" writer.WriteArrayHeader({document.Members.Length});");
|
||||
|
|
@ -305,7 +305,7 @@ public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator
|
|||
{
|
||||
builder.AppendLine($" private static object {document.PayloadDeserializerMethodName}(byte[] payload)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.Options;");
|
||||
builder.AppendLine($" var options = global::GameCult.Caching.MessagePack.CultDocumentMessagePackSerialization.OptionsFor(typeof({document.DocumentTypeName}).Assembly);");
|
||||
builder.AppendLine(" var reader = new global::MessagePack.MessagePackReader(payload);");
|
||||
builder.AppendLine(" var count = reader.ReadArrayHeader();");
|
||||
builder.AppendLine($" var value = new {document.DocumentTypeName}();");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace GameCult.Caching.MessagePack;
|
||||
|
||||
// The resolver type exposes a public static Instance of IFormatterResolver or a public parameterless constructor.
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CultCacheFormatterResolverAttribute : Attribute
|
||||
{
|
||||
public CultCacheFormatterResolverAttribute(Type resolverType)
|
||||
{
|
||||
ResolverType = resolverType ?? throw new ArgumentNullException(nameof(resolverType));
|
||||
}
|
||||
|
||||
public Type ResolverType { get; }
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
|
|
@ -32,12 +35,39 @@ public static class CultDocumentMessagePackSerialization
|
|||
private const int SchemaCatalogMemberFieldCount = 8;
|
||||
private const int StoreSnapshotFieldCount = 3;
|
||||
|
||||
public static readonly MessagePackSerializerOptions Options =
|
||||
MessagePackSerializerOptions.Standard
|
||||
public static readonly MessagePackSerializerOptions Options = Compose(Array.Empty<IFormatterResolver>());
|
||||
|
||||
private static readonly ConcurrentDictionary<Assembly, MessagePackSerializerOptions> AssemblyOptions = new();
|
||||
|
||||
public static MessagePackSerializerOptions OptionsFor(Assembly documentAssembly)
|
||||
{
|
||||
if (documentAssembly == null) throw new ArgumentNullException(nameof(documentAssembly));
|
||||
return AssemblyOptions.GetOrAdd(documentAssembly, static assembly =>
|
||||
{
|
||||
var resolvers = assembly.GetCustomAttributes<CultCacheFormatterResolverAttribute>()
|
||||
.Select(attribute => CreateResolver(attribute.ResolverType))
|
||||
.ToArray();
|
||||
return resolvers.Length == 0 ? Options : Compose(resolvers);
|
||||
});
|
||||
}
|
||||
|
||||
private static MessagePackSerializerOptions Compose(IFormatterResolver[] consumerResolvers)
|
||||
{
|
||||
return MessagePackSerializerOptions.Standard
|
||||
.WithResolver(CompositeResolver.Create(
|
||||
CultDocumentResolver.Instance,
|
||||
StandardResolver.Instance))
|
||||
.WithSecurity(MessagePackSecurity.UntrustedData);
|
||||
consumerResolvers.Append(CultDocumentResolver.Instance).Append(StandardResolver.Instance).ToArray()))
|
||||
.WithSecurity(CultMessagePackSecurity.Instance);
|
||||
}
|
||||
|
||||
private static IFormatterResolver CreateResolver(Type resolverType)
|
||||
{
|
||||
var instance = resolverType.GetField("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null)
|
||||
?? resolverType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null)
|
||||
?? (resolverType.GetConstructor(Type.EmptyTypes) != null ? Activator.CreateInstance(resolverType) : null);
|
||||
return instance as IFormatterResolver
|
||||
?? throw new InvalidOperationException(
|
||||
$"Formatter resolver {resolverType.FullName} needs a public static Instance or a public parameterless constructor and must implement {nameof(IFormatterResolver)}.");
|
||||
}
|
||||
|
||||
public static byte[] Serialize<T>(T value)
|
||||
{
|
||||
|
|
@ -66,7 +96,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
}
|
||||
}
|
||||
|
||||
return MessagePackSerializer.Serialize(type, value, Options);
|
||||
return MessagePackSerializer.Serialize(type, value, OptionsFor(type.Assembly));
|
||||
}
|
||||
|
||||
public static object DeserializeUntyped(Type type, byte[] payload)
|
||||
|
|
@ -83,7 +113,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
return descriptor.GeneratedPayloadDeserializer(payload);
|
||||
}
|
||||
|
||||
return MessagePackSerializer.Deserialize(type, payload, Options)
|
||||
return MessagePackSerializer.Deserialize(type, payload, OptionsFor(type.Assembly))
|
||||
?? throw new InvalidOperationException($"MessagePack returned null for Cult document type {type.FullName}.");
|
||||
}
|
||||
|
||||
|
|
|
|||
35
src/GameCult.Caching.MessagePack/CultMessagePackSecurity.cs
Normal file
35
src/GameCult.Caching.MessagePack/CultMessagePackSecurity.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MessagePack;
|
||||
|
||||
namespace GameCult.Caching.MessagePack;
|
||||
|
||||
// UntrustedData, plus a collision-resistant comparer for CultRecordRef<T> dictionary keys over the key string.
|
||||
public sealed class CultMessagePackSecurity : MessagePackSecurity
|
||||
{
|
||||
public static readonly CultMessagePackSecurity Instance = new(UntrustedData);
|
||||
|
||||
private CultMessagePackSecurity(MessagePackSecurity copyFrom) : base(copyFrom) { }
|
||||
|
||||
protected override IEqualityComparer<T> GetHashCollisionResistantEqualityComparer<T>()
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(CultRecordRef<>))
|
||||
return base.GetHashCollisionResistantEqualityComparer<T>();
|
||||
var comparerType = typeof(RecordRefComparer<>).MakeGenericType(type.GetGenericArguments()[0]);
|
||||
return (IEqualityComparer<T>)Activator.CreateInstance(comparerType, GetEqualityComparer<string>())!;
|
||||
}
|
||||
|
||||
protected override MessagePackSecurity Clone() => new CultMessagePackSecurity(this);
|
||||
|
||||
private sealed class RecordRefComparer<TDocument> : IEqualityComparer<CultRecordRef<TDocument>>
|
||||
{
|
||||
private readonly IEqualityComparer<string> _keys;
|
||||
|
||||
public RecordRefComparer(IEqualityComparer<string> keys) => _keys = keys;
|
||||
|
||||
public bool Equals(CultRecordRef<TDocument> x, CultRecordRef<TDocument> y) => _keys.Equals(x.Key.Value, y.Key.Value);
|
||||
|
||||
public int GetHashCode(CultRecordRef<TDocument> value) => _keys.GetHashCode(value.Key.Value);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue