rebus-org / Rebus.Async

:bus: Experimental async extensions for Rebus
https://mookid.dk/category/rebus
Other
13 stars 9 forks source link

SendRequest fails with 'Could not load file or assembly' #13

Closed marrrschine closed 3 years ago

marrrschine commented 4 years ago

Hi,

I am trying to establish request/response between two isolated/independent .NET Core services leveraging on RabbitMQ as transport.

Sending an event like

await _bus.SendRequest<SomeCompany.SomeContext.Messages.SomeEvent>(...)

fails with

Could not deserialize JSON text: '{"$type":"SomeCompany.SomeContext.Messages.SomeEvent, AssemblyName","Value":"someValue"}'
---> Newtonsoft.Json.JsonSerializationException: Error resolving type specified in JSON 'SomeCompany.SomeContext.Messages.SomeEvent, AssemblyName'. Path '$type', line 1, position 96. 
---> System.IO.FileNotFoundException: Could not load file or assembly 'AssemblyName, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.

So what I basically tried was to configure the bus with the option

o.Register<ITopicNameConvention>(c => new SimpleTopicNameConvention());

as I read something similar in this issue.

But I can observe GetTopic() of SimpleTopicNameConvention is not called, which in turn means that I cannot adjust the behavior of how topics are going to be created with this approach. So I guess it is the wrong approach.

Can you help me out? Thanks in advance!

marrrschine commented 4 years ago

Finally made it. There's no need for the ITopicNameConvention - but there are two more questions now.

Since updating to Rebus 6.1.0 I'm able to configure the serializer regarding to my needs. For service 1 (Producer), in Startup.cs (by Rebus.ServiceProvider) this is:

services.AddRebus(configure => configure
                .Logging(l =>
                {
                    l.Use(new LoggerFactoryAdapter(factory));
                })
                .Options(o =>
                {
                    o.EnableSynchronousRequestReply();
                })
                .Transport(t =>
                {
                    t.UseRabbitMq(connectionString, $"{exchange}").ExchangeNames(topicExchangeName: $"{exchange}");
                })
                .Serialization(s =>
                {
                    s.UseNewtonsoftJson(JsonInteroperabilityMode.PureJson);
                    s.UseCustomMessageTypeNames()
                        .AddWithCustomName<ProducerContext.SomeEvent>("ConsumerContext.SomeEvent, ConsumerAssemblyName")
                        .AllowFallbackToDefaultConvention();
                })
                .Routing(r =>
                {
                    r.TypeBased()
                        .Map<ProducerContext.SomeEvent>("consumer");
                }));

Service 2 (Consumer) is configured like:

            var BusAdapter = new BuiltinHandlerActivator();
            BusAdapter.Handle<ConsumerContext.SomeEvent>(async (bus, query) => await HandleQuery(bus, query));

            var Bus = Configure.With(BusAdapter)
                    .Transport(t =>
                    {
                        t.UseRabbitMq(connectionString, $"{exchange}").ExchangeNames(topicExchangeName: $"{exchange}");
                    })
                    .Serialization(s =>
                    {
                        s.UseNewtonsoftJson(JsonInteroperabilityMode.PureJson);
                        s.UseCustomMessageTypeNames()
                            .AddWithCustomName<ConsumerContext.SomeResponse>("ProducerContext.SomeResponse, ProducerAssemblyName")
                            .AllowFallbackToDefaultConvention();

                    })
                    .Routing(r =>
                    {
                        r.TypeBased()
                           .Map<ConsumerContext.SomeEvent>("consumer");
                    })
                    .Start();

and the handler method

private async Task HandleQuery(IBus bus, object query)
        {
            await bus.Reply(new SomeResponse()
            {
               ...
            });
        }

First question:

On sending the event by the Producer with

var response = await _bus
    .SendRequest<ProducerContext.SomeResponse>
        (
            new ProducerContext.SomeEvent(param),
        );

the respective HandleQuery() in Consumer gets called, but the SomeEvent param is null. Can you see if I forgot something? The response received back is fine by the way.

Second question:

As soon as the number of different events increases, managing UseCustomMessageTypeNames will get complex and error-prone due to the manual input of the name strings.

Are there any best practices in dealing with increasing dynamics/variety of events?

mookid8000 commented 3 years ago

(..) but the SomeEvent param is null

From the JSON you posted earlier

{"$type":"SomeCompany.SomeContext.Messages.SomeEvent, AssemblyName","Value":"someValue"}

it looks like SomeEvent has a Value property of type string. In your code sample, I can see that you pass the param into the constructor of SomeEvent like so: new ProducerContext.SomeEvent(param)

My guess is that the constructor of SomeEvent does NOT have a string value parameter corresponding to the Value property like so:

public class SomeEvent
{
    public SomeEvent(string value) => Value = value;
    public string Value { get; }
}

but instead uses another name in the constructor, e.g. like this:

public class SomeEvent
{
    public SomeEvent(string doesNotWork) => Value = doesNotWork;
    public string Value { get; }
}

As soon as the number of different events increases, managing UseCustomMessageTypeNames will get complex and error-prone due to the manual input of the name strings. Are there any best practices in dealing with increasing dynamics/variety of events?

Well, if you identify this as an axis out of which your system is going to grow, I suggest you come up with some kind of systematic approach towards naming your events – a "convention", if you like.. You can then use some code to enforce that convention by reflecting over your message assemblies, and then you can also code something that automatically configures the name of all of your types.