When you’re building messaging systems, I bet you’ve run into a situation where you’ve had the need to pass some extra information along with your messages: Some kind of metadata that plays a role in addressing a cross-cutting concern, like e.g. bringing along contextual information about the user that initiated the current message correspondence.
To show how that can easily be achieved with Rebus, I’ve added the UserContextHeaders sample to the sample repository. This sample demonstrates how an “ambient user context” can be established in a client application which then automagically flows along with all messages sent as a consequence of the initiating message.
I this blog post, I’ll go through the key parts that make up the solution – if you’re interested in the details, I suggest you go download the source code.
First, let’s
See how Rebus is configured
1 2 3 4 5 6 7 8 |
Configure.With(new WindsorContainerAdapter(appContainer)) .Logging(l => l.ColoredConsole(minLevel: LogLevel.Warn)) .Transport(t => t.UseMsmq("sample.input", "sample.error")) .AutomaticallyTransferUserContext() //< this one is special :) .CreateBus() .Start(); |
As you can see, I’ve added some extra setup in an extension method called AutomaticallyTransferUserContext – it looks like this:
1 2 3 4 5 6 7 8 |
public static RebusConfigurer AutomaticallyTransferUserContext(this RebusConfigurer configurer) { return configurer.Events(e => { e.MessageSent += EncodeUserContextHeadersIfPossible; e.MessageContextEstablished += DecodeUserContextHeadersIfPossible; }); } |
It’s pretty clear that it hooks into Rebus’ MessageSent and MessageContextEstablished events which are called for all outgoing messages and all incoming messages respectively.
First, let’s see what we must do when we’re
Sending messages
Let’s see what’s inside that EncodeUserContextHeadersIfPossible method from the snippet above:
1 2 3 4 5 6 7 8 9 |
static void EncodeUserContextHeadersIfPossible(IBus bus, string destination, object message) { var current = AmbientUserContext.Current; if (current == null) return; var encodedUserContext = Encode(current); bus.AttachHeader(message, UserContextHeaderKey, encodedUserContext); } |
As you can see, it checks for the presence of an ambient user context (most likely attached to the currently executing thread), encoding that context in a message header if one was found. This way, the user context will be included on all outgoing messages, no matter if they’re sent, published or replied.
Now, let’s see how we’re
Handling incoming messages
Let’s just check out DecodeUserContextHeadersIfPossible mentioned in the configuration extension above:
1 2 3 4 5 6 7 8 9 |
static void DecodeUserContextHeadersIfPossible(IBus bus, IMessageContext messageContext) { var headers = messageContext.Headers; if (!headers.ContainsKey(UserContextHeaderKey)) return; var encodedUserContext = (string) headers[UserContextHeaderKey]; messageContext.Items[UserContextItemKey] = Decode(encodedUserContext); } |
As you can see, it checks for the presence of a user context header on the incoming message, decoding it and adding it to the message context if it was there. This means that message handlers can access the context like this:
1 2 3 4 5 6 7 |
public void Handle(SomeMessage doesNotMatter) { var messageContext = MessageContext.GetCurrent(); var userContext = (UserContext)messageContext.Items[UserContextItemKey]; // do stuff with user context } |
These were the main parts that make up the automatically flowing user context – there’s few more details in the sample code, like e.g. an implementation of a thread-bound ambient user context and constructor-injected UserContext into message handlers – pretty sweet, if I have to say so myself 🙂 please head over to the UserContextHeaders sample for some working code to look at.