3

Tree Structure

As you see there are a Headquarter as root node and some branches as child nodes. There is a message of Data type, and I want to publish a message based on content of Data object, for example :

if (data.value == xxxx) publish(data, Br1, Br2)
else if (data.value == yyyy) publish(data, Br3, Br4)
else if (data.value == zzzz) publis(data, Br5, Br6)

This is somehow customized version of pub/sub pattern. But I want publish message of type Data to just some special subscribers based on content of message.

Is there a solution in Rebus?

Saeed
  • 185
  • 2
  • 13

4 Answers4

4

There's several solutions in Rebus :)

For your scenario, I can see two ways of solving it: 1) Use custom topics, or 2) Implement a real content-based router.

If it makes sense, you can model this pub/sub scenario using topics by using Rebus' topics API to take care of the routing. This makes sense if you can say that your data messages each belong to some category, which your subscribers can then subscribe to.

Compared to "real" topics-based queueing systems, like e.g. RabbitMQ, the topics API in Rebus is very crude. It doesn't allow for wildcards(*) or anything advanced like that – topics are just simple strings that you can subscribe to and then use as a pub/sub channel to have an event routed to multiple subscribers.

You can use it like this in the subscriber's end:

await bus.Advanced.Topics.Subscribe("department_a");

and then in the publisher's end:

var data = new Data(...);

await bus.Advanced.Topics.Publish("department_a", data);

If that doesn't cut it, you can insert a "real" content-based router which is simply an endpoint to which you await bus.Send(eachDataMessage), which in turn forwards the message to the relevant subscribers.

It can be done at two levels with Rebus, depending on your requirements. If it is enough to look at the message's headers, you should implement it as a "transport message forwarder", because that skips deserialization and provides a nice API for simply forwarding messages:

Configure.With(...)
    .Transport(t => t.UseMsmq("router"))
    .Routing(r => {
        r.AddTransportMessageForwarder(async transportMessage => {
            var headers = transportMessage.Headers;

            var subscribers = Decide(headers);

            return ForwardAction.ForwardTo(subscribers);
        });
    })
    .Start();

If you need to look at the actual message, you should just implement an ordinary message handler and then use the bus to forward the message:

public class Router : IHandleMessages<Data>
{
    readonly IBus _bus;

    public Router(IBus bus)
    {
        _bus = bus;
    }   

    public async Task Handle(Data message)
    {
        var subscribers = Decide(message);

        foreach(var subscriber in subscribers)
        {
            await _bus.Advanced.TransportMessage.ForwardTo(subscriber);
        }
    }
}

The custom-implemented router is the most flexible solution, as you can implement any logic you like, but as you can see it is slightly more involved.


(*) Rebus doesn't allow for using wildcards in general, although it does pass the topics directly to RabbitMQ if you happen to be using that as the transport, which means that you can actually take full advantage of RabbitMQ (see this issue for some more info about that)

mookid8000
  • 18,258
  • 2
  • 39
  • 63
  • Thanks. Regarding the last solution (router/handler), I wrote these lines of codes, but I got exception in handler after forwarding some packet objects. (bus is null) – Saeed Nov 21 '16 at 16:52
0
    static void Main()
    {

        using (var activator = new BuiltinHandlerActivator())
        {
            activator.Handle<Packet>(async (bus, packet) =>
            {
                string subscriber = "subscriberA";
                await bus.Advanced.TransportMessage.Forward(subscriber); 
            });

            Configure.With(activator)
                .Logging(l => l.ColoredConsole(minLevel: LogLevel.Warn))
                .Transport(t => t.UseMsmq("router"))
                .Start();

            for (int i = 0; i < 10; i++)
            {
                activator.Bus.SendLocal(
                    new Packet()
                    {
                        ID = i,
                        Content = "content" + i.ToString(),
                        Sent = false,
                    }).Wait();
            }
        }

        Console.ReadLine();
    }
Saeed
  • 185
  • 2
  • 13
  • You get a `NullReferenceException` because you dispose the `BuiltinHandlerActivator` much too early – if you `Console.ReadLine();` just after existing the `for`-loop, you will get another error stating that the queue `subscriberA` does not exist – mookid8000 Nov 21 '16 at 17:41
  • Silly me. Thanks. :) – Saeed Nov 22 '16 at 07:33
  • yes :) you can read about it [here on the wiki page about transactions](https://github.com/rebus-org/Rebus/wiki/Transactions) – mookid8000 Nov 22 '16 at 14:53
  • Thanks a lot for you help. :) The piece of codes at the last comment is my subscriber side. My subscriber consumes a message in a transaction scope because it deals with DB. Packets sent from publisher must be persisted in subscriber DB. Everything's working fine. But sometimes, I kill the subscriber console app manually and then run it again (simulating a fatal error in consumer app), and after repeating this, PK_Violation error raised, means that the message was handled before and in the next run, it raises exception. Did I miss something? – Saeed Nov 23 '16 at 17:27
0
using (var trScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
   scope.EnlistRebus();
   Packet packet = ReadFromDB()
   activator.Bus.SendLocal(packet).Wait()
   scope.Complete()
}


activator.Handle<Packet>(async (bus, packet) =>
{
   string subscriber = "subscriberA";
   await bus.Advanced.TransportMessage.Forward(subscriber); 
});
Saeed
  • 185
  • 2
  • 13
0
        using (var activator = new BuiltinHandlerActivator())
        {
            activator.Handle<Packet>(async message =>
            {
                string connectionString =
                    "Data Source=.;Initial Catalog=Rebus;User ID=sa;Password=123456";

                using (SqlConnection connection = new SqlConnection(connectionString))
                {
                    string queryString = @"INSERT INTO CLIENTPACKET(ID, CONTENT, SENT) VALUES(@id, @content, @sent)";
                    connection.Open();

                    using (SqlCommand command = new SqlCommand(queryString, connection))
                    {
                        command.Parameters.Add(new SqlParameter("@id", message.ID));
                        command.Parameters.Add(new SqlParameter("@content", message.Content));
                        command.Parameters.Add(new SqlParameter("@sent", message.Sent));

                        await command.ExecuteNonQueryAsync();
                    }
                }
            });


            Configure.With(activator)
                .Logging(l => l.ColoredConsole(minLevel: LogLevel.Warn))
                .Transport(t => t.UseMsmq(@"subscriberA"))
                .Routing(r => r.TypeBased().MapAssemblyOf<Packet>("router"))
                .Options(o =>
                {
                    TransactionOptions tranOp = new TransactionOptions();
                    tranOp.IsolationLevel = IsolationLevel.ReadCommitted;
                    o.HandleMessagesInsideTransactionScope(tranOp);

                    o.SetNumberOfWorkers(2);
                    o.SetMaxParallelism(2);
                })
                .Start();

            activator.Bus.Subscribe<Packet>().Wait();

            Console.WriteLine("Press ENTER to quit");
            Console.ReadLine();
        }
Saeed
  • 185
  • 2
  • 13