Reputation: 1036
I am implementing a system based on SignalR that will be pushing client events, for message serialization I would want to use MessagePack. When trying to implement the messaging I have run into a problem where SignalR fails to deserialize the messages on the client.
The messages are polymorphic and described with Union attributes, the standard MessagePack Serializer have no problem serializing and deserializing the messages BUT in case of Signal R it fails with error.
The error reported is System.InvalidOperationException: Invalid Union data was detected.
On the client serialization only works when using the actual class, if I try to use the interface or base class then the error appears.
Classes
[DataContract()]
[MessagePackObject()]
[Union(0,typeof(EntityChangeEventMessage))]
public abstract class EntityEventMessage : IEntityEventMessage
{
}
[DataContract()]
[MessagePackObject()]
public class EntityChangeEventMessage : EntityEventMessage
{
#region PROPERTIES
/// <summary>
/// Gets entity id.
/// </summary>
[DataMember(Order = 1)]
[Key(1)]
public int EntityId
{
get; set;
}
/// <summary>
/// Gets event type.
/// </summary>
/// <remarks>
/// This value identifies database operation such as create,delete,update etc.
/// </remarks>
[DataMember(Order = 2)]
[Key(2)]
public int EventType
{
get; set;
}
/// <summary>
/// Gets entity type name.
/// </summary>
[DataMember(Order = 3)]
[Key(3)]
public string EntityType
{
get; set;
}
#endregion
}
[Union(0,typeof(EntityChangeEventMessage))]
public interface IEntityEventMessage
{
}
So this works
connection.On("EntityEvent", (EntityChangeEventMessage d)
This dont work
connection.On("EntityEvent", (IEntityEventMessaged)
So in general it looks like the problem should be in the Microsoft.AspNetCore.SignalR.Protocols.MessagePack library ? Anyone have implemented such functionality with success ?
Upvotes: 2
Views: 1155
Reputation: 918
I know this question is 4 years old, but is still at the top of Google results so here is my workaround
public enum MessageType
{
MessageA = 1,
MessageB,
}
[MessagePackFormatter(typeof(MessageFormatter))]
public interface IMessage
{
MessageType Type { get; set; }
string Name { get; set; }
}
[DataContract]
public class MessageA : IMessage
{
[DataMember(Name = "type")]
public MessageType Type { get; set; }
[DataMember(Name = "name")]
public required string Name { get; set; }
[DataMember(Name = "propA")]
public required string PropA { get; set; }
}
[DataContract]
public class MessageB : IMessage
{
[DataMember(Name = "type")]
public MessageType Type { get; set; }
[DataMember(Name = "name")]
public required string Name { get; set; }
[DataMember(Name = "propB")]
public required string PropB { get; set; }
}
internal class PolymorphicResolver : IFormatterResolver
{
public static readonly IFormatterResolver Instance = new PolymorphicResolver();
private static object? GetFormatter(Type t)
{
MessagePackFormatterAttribute? formatterAttribute = (MessagePackFormatterAttribute?)Attribute.GetCustomAttribute(t, typeof(MessagePackFormatterAttribute));
if (formatterAttribute == null)
{
return null;
}
Type formatterType = formatterAttribute.FormatterType;
object? formatterInstance = Activator.CreateInstance(formatterType);
return formatterInstance;
}
private PolymorphicResolver()
{
}
public IMessagePackFormatter<T>? GetFormatter<T>()
{
return FormatterCache<T>.Formatter;
}
private static class FormatterCache<T>
{
public static readonly IMessagePackFormatter<T>? Formatter;
static FormatterCache()
{
Formatter = (IMessagePackFormatter<T>?)GetFormatter(typeof(T));
}
}
}
class MessageFormatter : IMessagePackFormatter<IMessage>
{
public void Serialize(
ref MessagePackWriter writer, IMessage value, MessagePackSerializerOptions options)
{
MessagePackSerializer.Serialize(value.GetType(), ref writer, value, options);
}
public IMessage Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
throw new InvalidOperationException($"Failed to deserialize {nameof(IMessage)}, received null");
}
ReadOnlySequence<byte> rawSequence = reader.ReadRaw();
MessagePackReader tempReader = new(rawSequence);
// Read the map header to know how many keys there are.
int propertyCount = tempReader.ReadMapHeader();
MessageType? messageType = null;
// Iterate through the map properties to find the "type" field.
for (int i = 0; i < propertyCount; i++)
{
// Read the property key as string.
string? key = tempReader.ReadString();
Debug.Assert(key is not null, $"{nameof(key)} is not null");
if (key.Equals(nameof(IMessage.Type), StringComparison.CurrentCultureIgnoreCase))
{
messageType = (MessageType)tempReader.ReadInt32();
break;
}
// Skip the value for keys that we are not interested in.
tempReader.Skip();
}
if (messageType == null)
{
throw new MessagePackSerializationException($"Missing required field {nameof(IMessage.Type).ToLower()} to determine the IMessage type.");
}
IMessage result = messageType switch
{
MessageType.MessageA => MessagePackSerializer.Deserialize<MessageA>(rawSequence, options),
MessageType.MessageB => MessagePackSerializer.Deserialize<MessageB>(rawSequence, options),
_ => throw new ArgumentOutOfRangeException()
};
return result;
}
}
public class CommandsHub : Hub
{
public async Task<IMessage> Test(IMessage message,
[FromServices] ILogger<CommandsHub> logger)
{
logger.LogTrace("Test : {message}", message.Name);
if (message is MessageA messageA)
{
logger.LogTrace("PropA {propA}", messageA.PropA);
}
if (message is MessageB messageB)
{
logger.LogTrace("PropA {propB}", messageB.PropB);
}
return message;
}
}
In Program.cs
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.SerializerOptions = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(PolymorphicResolver.Instance, ContractlessStandardResolver.Instance));
})
.AddHubOptions<CommandsHub>((x) => x.EnableDetailedErrors = true);
Client side, in my case Typescript
export enum MessageType {
MessageA = 1,
MessageB = 2,
}
export interface IMessage {
type: MessageType,
name: string
}
export interface MessageA extends IMessage {
propA: string
}
export interface MessageB extends IMessage {
propB: string
}
export class Backend {
_connection: HubConnection
constructor() {
this._connection = new HubConnectionBuilder()
.withUrl("https://localhost:7121/commands")
.withHubProtocol(new MessagePackHubProtocol())
.configureLogging(LogLevel.Information)
.build()
this._connection.onclose((error?: Error) => {
console.log(`onclose. error: ${error}`)
})
}
async start() {
try {
await this._connection.start()
} catch (err) {
console.log(err)
}
}
async test(message: IMessage): Promise<IMessage | undefined> {
if (this._connection.state !== "Connected") {
await this.start()
}
try {
return await this._connection.invoke<IMessage>("Test", message)
} catch (err) {
console.error(err)
return undefined
}
}
}
The call from client side
<button type="button"
onClick={async () => {
const messageA: MessageA = { type: MessageType.MessageA, name: "name a", propA: "a" }
const response: IMessage | undefined = await backend.test(messageA)
console.log(response)
}}
>
Test A
</button>
<button type="button"
onClick={async () => {
const messageB: MessageB = { type: MessageType.MessageB, name: "name b", propB: "b" }
const response: IMessage | undefined = await backend.test(messageB)
console.log(response)
}}
>
Test B
</button>
Upvotes: 0
Reputation: 1036
Currently SignalR does not support polymorphism with MessagePack, more info here.
Upvotes: 0