NullReference
NullReference

Reputation: 1036

SignalR MessagePack Polymorphism

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

Answers (2)

KVM
KVM

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

NullReference
NullReference

Reputation: 1036

Currently SignalR does not support polymorphism with MessagePack, more info here.

Upvotes: 0

Related Questions