Saturday 9 November 2013

Changing the ways queues are named in EasyNetQ

Update: In the time it took me to make the change to our code and write the blog post below, EasyNetQ got updated, thus rendering this post obsolete.  I'll soon be doing a new version that uses the latest version.

At my current company (DrDoctor) we're using EasyNetQ as a lightweight bus implementation on top of RabbitMQ. EasyNetQ has a lot of very nice features, one of which is auto-creation of exchanges and queues. It uses the fully qualified name of the message type as its basis, but we have quite a deep hierarchy of messages, which makes sense in the tree-structure of our solution, but makes looking at the queues painful as we end up with long names.

Fortunately EasyNetQ is very extensible which allows you to change those conventions (and many others). The basis for this is taken from the documention: https://github.com/mikehadlow/EasyNetQ/wiki/Replacing-EasyNetQ-Components

The 'CreateBus' method has an overload that allows you to pass in your own services. When 'CreateBus' is called, if you pass in your own service (for instance IEasyNetQLogger from the example in the documention), your service gets registered first and so the default service registration doesn't happen (take a look at DefaultServiceProvider.Register for details).

In our case we wanted to alter the way queues and exchanges are named, which is slightly more complex because there isn't a separate service for each of those, instead there is a 'Conventions' service which sets these. However, that service is itself easy to poke from the outside. Here's some code:
 IConventions conventions = new Conventions();
conventions.QueueNamingConvention = (messageType, subscriptionId) =>
{
    var queuePrefix = EasyNetQNamingConvention.GetNameFromType(messageType);
    return string.Format("{0}:{1}", queuePrefix, subscriptionId);
};

conventions.ExchangeNamingConvention = EasyNetQNamingConvention.GetNameFromType;            
RabbitHutch.CreateBus(connectionString.ConnectionString, serviceRegister => serviceRegister.Register(provider => conventions));


The method 'GetNameFromType' is a static method that returns a string, based on the type passed in.
public static string GetNameFromType(Type type)
{
 if (type.GetInterface("IMyMessage") == null)
  throw new ArgumentException("Type must implement IMyMessage");

 string category = null;

 foreach (var attr in type.GetCustomAttributes(false).OfType<MessageCategoryAttribute>())
 {
  category = attr.Category;
 }

 if (category == null)
  throw new ArgumentException("Implementation must be marked with MessageCategoryAttribute");

 return category + "_" + type.Name;
}
This suits our needs as it means we can (and indeed have to) mark up our messages with an attribute that categorises them.

There is a bit of gotcha with this, however. We have effectively replaced the use of the EasyNetQ method 'TypeNameSerializer.Serialize' for our exchanges and message queue names, which is fine, but this method is used elsewhere that could cause a problem. There is another service that is registered
.Register<SerializeType>(x => TypeNameSerializer.Serialize)
, and SerializeType is used in DefaultMessageValidationStrategy.

Again though the solution is simple, substitute in your own implementation:

RabbitHutch.CreateBus(connectionString.ConnectionString, serviceRegister =>
 {
  serviceRegister.Register(provider => conventions);
  serviceRegister.Register<SerializeType>(provider => EasyNetQNamingConvention.GetEasyNetQNameFromType);
 });
);

No comments:

Post a Comment