Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HelloWorld example for sending an object over RabbitMQ via EasyNetQ between two different applications

Hi I am attempting to send a simple object like through RabbitMQ via EasyNetQ. I'm having issues deserializing that object on the subscription side. Anyone able to show me a sample of how this works. Keep in mind the object being sent is defined in it's own project and not shared among the publisher and subscriber. Here is my sample, and perhaps you can tell me what is wrong with it?

Program A:

class ProgramA
{
    static void Main(string[] args)
    {
        using (var bus = RabbitHutch.CreateBus("host=localhost"))
        {
            Console.WriteLine("Press any key to send the message");
            Console.ReadKey();
            bus.Publish(new MessageA { Text = "Hello World" });
            Console.WriteLine("Press any key to quit");
            Console.ReadKey();
        }
    }

    public class MessageA
    {
        public string Text { get; set; }
    }
}

Program B:

class ProgramB
{
    static void Main(string[] args)
    {
        using (var bus = RabbitHutch.CreateBus("host=localhost"))
        {
            bus.Subscribe<MessageB>("", HandleClusterNodes);
            Console.WriteLine("Press any key to quit");
            Console.ReadKey();
        }
    }

    private static void HandleClusterNodes(MessageB obj)
    {
        Console.WriteLine(obj.Text);
    }

    [Queue("TestMessagesQueue", ExchangeName = "EasyNetQSample.ProgramA+MessageA:EasyNetQSample")]
    public class MessageB
    {
        public string Text { get; set; }
    }
}

Here is the error I'm receiving:

DEBUG: HandleBasicDeliver on consumer: f9ded52d-039c-411a-9b9f-5c8ee3301854, deliveryTag: 1
DEBUG: Received
        RoutingKey: ''
        CorrelationId: 'ec41faea-a0c8-4ffd-8163-2cbf85d45fcd'
        ConsumerTag: 'f9ded52d-039c-411a-9b9f-5c8ee3301854'
        DeliveryTag: 1
        Redelivered: False
ERROR: Exception thrown by subscription callback.
        Exchange:    'EasyNetQSample.ProgramA+MessageA:EasyNetQSample'
        Routing Key: ''
        Redelivered: 'False'
Message:
{"Text":"Hello World"}
BasicProperties:
ContentType=NULL, ContentEncoding=NULL, Headers=[], DeliveryMode=2, Priority=0, CorrelationId=ec41faea-a0c8-4ffd-8163-2cbf85d45fcd, ReplyTo=NULL, Expiration=NULL, MessageId=NULL, Timestamp=0, Type=EasyNetQSample.ProgramA+MessageA:EasyNetQSample, UserId=NULL, AppId=NULL, ClusterId=NULL
Exception:
System.AggregateException: One or more errors occurred. ---> EasyNetQ.EasyNetQException: Cannot find type EasyNetQSample.ProgramA+MessageA:EasyNetQSample
   at EasyNetQ.TypeNameSerializer.DeSerialize(String typeName)
   at EasyNetQ.DefaultMessageSerializationStrategy.DeserializeMessage(MessageProperties properties, Byte[] body)
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass19.<Consume>b__18(Byte[] body, MessageProperties properties, MessageReceivedInfo messageReceivedInfo)
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass1e.<Consume>b__1d(Byte[] body, MessageProperties properties, MessageReceivedInfo receviedInfo)
   at EasyNetQ.Consumer.HandlerRunner.InvokeUserMessageHandler(ConsumerExecutionContext context)
   --- End of inner exception stack trace ---
---> (Inner Exception #0) EasyNetQ.EasyNetQException: Cannot find type EasyNetQSample.ProgramA+MessageA:EasyNetQSample
   at EasyNetQ.TypeNameSerializer.DeSerialize(String typeName)
   at EasyNetQ.DefaultMessageSerializationStrategy.DeserializeMessage(MessageProperties properties, Byte[] body)
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass19.<Consume>b__18(Byte[] body, MessageProperties properties, MessageReceivedInfo messageReceivedInfo)
   at EasyNetQ.RabbitAdvancedBus.<>c__DisplayClass1e.<Consume>b__1d(Byte[] body, MessageProperties properties, MessageReceivedInfo receviedInfo)
   at EasyNetQ.Consumer.HandlerRunner.InvokeUserMessageHandler(ConsumerExecutionContext context)<---

What do I need to do to be able to properly deserialize MessageA?

like image 376
ymerej Avatar asked Oct 16 '14 22:10

ymerej


2 Answers

As far as I know, the default setting of the EasyNetQ requires the type of the serialized object to be consistent between applications. For example, you can send any known .NET type easily like String:

 bus.Publish<String>("Excellent.");

and it will be happy on both projects.

You can use your own Message if you put it to a common library (dll). Since you specially mentioned that they reside in different projects, I'd suggest to serialize and cast objects yourself.

EasyNetQ uses interally Newtonsoft Json.NET to serialize objects like this. As you can see, your message has been serialized already as:

Message: {"Text":"Hello World"}

To do this yourself, you still need to add a reference to Json.NET because EasyNetQ hides this reference by using ilrepack.

This should work:

bus.Publish<string>(JsonConvert.SerializeObject(new MessageA { Text = "Hello World" }));

and

bus.Subscribe<string>("", HandleClusterNodes);

private static void HandleClusterNodes(string obj)
{
    var myMessage = (MessageB)JsonConvert.DeserializeObject<MessageB>(obj);
    Console.WriteLine(myMessage.Text);
}

But you'll lose your attribute based routing with this and might want to modify your methods.

If you want to keep using basic methods, you can set topic with like this:

bus.Publish<string>(JsonConvert.SerializeObject(new MessageA { Text = "Hello World" }), "topic.name");

bus.Subscribe<string>("", HandleClusterNodes, new Action<EasyNetQ.FluentConfiguration.ISubscriptionConfiguration>( o => o.WithTopic("topic.name")));

But to have complete control, you need to use Advanced API;

var yourMessage = new Message<string>(JsonConvert.SerializeObject(new MessageA { Text = "Hello World" }));
bus.Advanced.Publish<string>(new Exchange("YourExchangeName"), "your.routing.key", false, false, yourMessage);

and on the subscriber part:

IQueue yourQueue = bus.Advanced.QueueDeclare("AnotherTestMessagesQueue");
IExchange yourExchange = bus.Advanced.ExchangeDeclare("YourExchangeName", ExchangeType.Topic);
bus.Advanced.Bind(yourExchange, yourQueue, "your.routing.key");
bus.Advanced.Consume<string>(yourQueue, (msg, info) => HandleClusterNodes(msg.Body));

which is the almost same as the original RabbitMQ C# Client API.


Detailed analysis:

The main problem is this exception:

EasyNetQ.EasyNetQException: Cannot find type EasyNetQSample.ProgramA+MessageA:EasyNetQSample

This is thrown by EasyNetQ because it cannot find the special class on the endpoint.

If we look at source code of the TypeNameSerializer.cs, you will see

var type = Type.GetType(nameParts[0] + ", " + nameParts[1]);
            if (type == null)
            {
                throw new EasyNetQException(
                    "Cannot find type {0}",
                    typeName);
            }

this is where it tried to find EasyNetQSample.ProgramA.MessageA type on second project, while it only knows EasyNetQSample.ProgramB.MessageB.

Alternatively, you can roll out your own custom ISerializer or put an ITypeNameSerializer into the default serializer but I haven't tried this.

like image 118
Furkan Omay Avatar answered Oct 25 '22 12:10

Furkan Omay


Furkan is correct that your problem is that the subscriber needs access to the MessageA type defined by your publisher so that it can deserialize the message to that type. From the EasyNetQ docs

When messages are serialized, EasyNetQ stores the message type name in the Type property of the message properties. This metadata is sent along with your message to any subscribers who can then use it to deserialize the message.

This amounts to a tight shared contract between publisher and consumer. If you want to loosen that up then there are a couple of things you can do:

  • You can publish and subscribe based on an interface (e.g. IMessage, from which you can derive MessageA). You will still need access to the MessageA type in order to cast the received message, but you can publish and subscribe without specifying a specific derived type.

  • You can create a single, shared "container" type (e.g. MessageContainer) and then serialize/deserialize your type into an instance of the container type as XML, JSON, whatever. Your subscriber can pull the data out of the container and parse it however it wants to. You could even include a schema or version info in the container header to give some hints to the subscriber about how to parse the data. The point it that they never have to turn it into a defined type and therefore don't need access to a bunch of types, just to the MessageContainer type so they can get at the serialized content.

like image 25
BitMask777 Avatar answered Oct 25 '22 12:10

BitMask777