MessagePack "shape provider is not valid for this cache" error when serializing interface inside object despite proper DerivedTypeMapping #362
|
Hi! I'm running into a serialization issue when trying to serialize an interface (IPayloadData) nested inside a class (Document), even though everything appears correctly mapped. Test casesHere's the test case that works: [Fact]
public void Test1()
{
IPayloadData sim = new SimulationContextData
{
Nodes = [new FlowchartSimulationNodeData { FigureId = Guid.NewGuid(), LuaScript = "print()" }],
Connections = []
};
SerializationConfigurator serializationConfigurator = new SerializationConfigurator();
SimulationSerializationConfigurator simulationSerializationConfigurator = new SimulationSerializationConfigurator();
var serializer = serializationConfigurator.CreateSerializer(simulationSerializationConfigurator.GetMappings());
var data = serializer.Serialize<IPayloadData, ContextWitness>(sim);
Assert.True(data != null);
}However, as soon as I wrap that same payload inside a Document class and try to serialize the whole Document, the serialization fails before it even accesses the Payloads dictionary: [Fact]
public void Test2()
{
IPayloadData sim = new SimulationContextData
{
Nodes = [new FlowchartSimulationNodeData { FigureId = Guid.NewGuid(), LuaScript = "print()" }],
Connections = []
};
Document document = new()
{
Canvas = new() { Settings = new() },
};
//document.Payloads.Add("Simulation", sim);
SerializationConfigurator serializationConfigurator = new SerializationConfigurator();
SimulationSerializationConfigurator simulationSerializationConfigurator = new SimulationSerializationConfigurator();
var serializer = serializationConfigurator.CreateSerializer(simulationSerializationConfigurator.GetMappings());
var data = serializer.Serialize<Document, DocumentWitness>(document);
var doc = serializer.Deserialize<Document, DocumentWitness>(data);
Assert.True(doc != null);
}Even when the Payloads dictionary is completely empty, the serialization still throws: Where It Fails (via debugger)The exception occurs during this shape generation call: private global::PolyType.Abstractions.IPropertyShape[] __CreateProperties_SimulationContextData() =>
new global::PolyType.Abstractions.IPropertyShape[] {
new SourceGenPropertyShape<SimulationContextData, string> {
Name = "Key",
...
AttributeProviderFunc = static () =>
typeof(SimulationContextData).GetProperty("Key", __BindingFlags_Instance_All, ...),
},
...
};The exception is thrown right at the first AttributeProviderFunc line. Even if i remove 'Key' property from IPayloadData it still throws exception either on 'Nodes' or 'Connections' My Modelspublic class Document
{
public required Canvas Canvas { get; set; }
public Dictionary<string, IPayloadData> Payloads { get; set; } = [];
}
public interface IPayloadData
{
string Key { get; }
}
public class SimulationContextData : IPayloadData
{
public const string PayloadKey = "Simulation";
public string Key => PayloadKey;
public required IEnumerable<SimulationNodeData> Nodes { get; set; }
public required IEnumerable<ConnectionData> Connections { get; set; }
}Shape witnesses[GenerateShape<IPayloadData>]
[GenerateShape<SimulationContextData>]
public partial class ContextWitness;
[GenerateShape<Domain.Document.Document>]
public partial class DocumentWitness;Serializer configuratorpublic class SimulationSerializationConfigurator
{
public IEnumerable<DerivedTypeMapping> GetMappings()
{
var nodeMapping = new DerivedShapeMapping<SimulationNodeData>();
nodeMapping.Add<FlowchartSimulationNodeData, NodeWitness>(1);
var contextMapping = new DerivedShapeMapping<IPayloadData>();
contextMapping.Add<SimulationContextData, ContextWitness>(1); // This causes the problem
return [nodeMapping, contextMapping];
}
}Workaround (kind of)If I comment out this line from configurator: Final questionWhy does the shape provider break even when all derived type mappings are present and the object works fine on its own? Is there some limitation with interface-based mapping inside other objects? Or does DerivedTypeMapping somehow interfere with resolution when used within a container class like Document? Any ideas or guidance would be hugely appreciated TL;DRI'm trying to serialize a Document that contains an interface (IPayloadData) with derived types registered via DerivedShapeMapping. |
Replies: 3 comments 1 reply
|
Tried to replicate this with tests with simpler objects and it works fine. using Nerdbank.MessagePack;
using PolyType;
namespace TestProject2
{
internal abstract class BaseB
{
public abstract string AlsoData { get; set; }
}
internal class B : BaseB
{
public override string AlsoData { get; set; } = "TEST";
}
internal interface IBaseA
{
string Data { get; set; }
}
internal class A : IBaseA
{
public string Data { get; set; } = string.Empty;
public IEnumerable<BaseB> SomeData { get; set; } = [];
}
internal class C
{
public Dictionary<string, IBaseA> Payload { get; set; } = [];
}
[GenerateShape<IBaseA>]
[GenerateShape<A>]
partial class AWitness;
[GenerateShape<BaseB>]
[GenerateShape<B>]
partial class BWitness;
[GenerateShape<C>]
partial class CWitness;
public class UnitTest1
{
private MessagePackSerializer CreateSerializer()
{
var serializer = new MessagePackSerializer()
{
SerializeDefaultValues = SerializeDefaultValuesPolicy.Always,
};
var mapping = new DerivedShapeMapping<IBaseA>();
mapping.Add<A, AWitness>(1);
var mapping2 = new DerivedShapeMapping<BaseB>();
mapping2.Add<B, BWitness>(1);
return serializer with { DerivedTypeMappings = [.. serializer.DerivedTypeMappings, mapping, mapping2] };
}
[Fact]
public void Test1()
{
var serializer = CreateSerializer();
var c1 = new C()
{
Payload = new Dictionary<string, IBaseA>() { { "Payload1", new A() { Data = "Key", SomeData = [new B(), new B()] } } }
};
var dataC1 = serializer.Serialize<C, CWitness>(c1);
}
}
}So now I don't see what I could be missing at all. And i have this project references: |
|
Ok, i'm pretty sure that's a bug. Test solution: |
|
Thanks for the excellent explanation and especially the minimal repro. This is by design. You should not make your Witness classes public. They should be internal and each assembly that needs to invoke serialization should be using its own witness class. I was able to get your failing test to pass by defining its own witness class, as follows: working testusing ChildApplication;
using ChildLibrary.Derived;
using ChildLibrary;
using Nerdbank.MessagePack;
using ParentLibrary;
using ParentApplication;
using PolyType;
namespace TestProjectWithDependencies
{
public class UnitTest1
{
private MessagePackSerializer CreateSerializer()
{
var serializer = new MessagePackSerializer()
{
SerializeDefaultValues = SerializeDefaultValuesPolicy.Always,
};
var mapping = new DerivedShapeMapping<IBaseA>();
mapping.Add<A, Witness>(1);
var mapping2 = new DerivedShapeMapping<BaseB>();
mapping2.Add<B, Witness>(1);
return serializer with { DerivedTypeMappings = [.. serializer.DerivedTypeMappings, mapping, mapping2] };
}
[Fact]
public void Test1()
{
var serializer = CreateSerializer();
var c1 = new C()
{
Payload = new Dictionary<string, IBaseA>() { { "Payload1", new A() { Data = "Key", SomeData = [new B(), new B()] } } }
};
var dataC1 = serializer.Serialize<C, Witness>(c1);
}
}
[GenerateShape<A>]
[GenerateShape<B>]
[GenerateShape<C>]
public partial class Witness;
}The problem is that you're mixing witnesses, and thereby mixing shape providers. There can only be one shape provider for an object graph (and probably for the whole serializer). |
Thanks for the excellent explanation and especially the minimal repro.
This is by design. You should not make your Witness classes public. They should be internal and each assembly that needs to invoke serialization should be using its own witness class. I was able to get your failing test to pass by defining its own witness class, as follows:
working test