Impersonating Events
Azure Service Bus queues and subscriptions are an excellent
way to process messages using competing consumers. But it
can also get really tricky. Let's look at a scenario where a
single event needs to be processed by two services. For this
example, I'll use a process of an agent being assigned to a
case. The requirement is pretty straightforward. When an
agent is assigned to a case, we should send an email
notifying the agent. In my system, I've designed it the way
that when the event of assignment
(AgentAssigned) is taking place, there are two
event handlers that would react to it:
- Update the querying data store with the information about the assignment to be able to look up agent assignments, and
- Notify the agent about the assignment with some case details.
It's all great except for one problem. When the second
handler runs first, there's still no association between the
agent and the case. No email can go out as there's nothing
to notify about. Or worse, when another event,
AgentReassigned, took place but hasn't been
processed by the first handler. In this case, we'd be
sending an email notification to the original agent who's no
longer on the case. The problem is quite apparent - we can't
have competing consumers for the same event. And the order
of execution is clearly essential.
One of the solutions is to introduce an additional event,
AgentAssignedCompleted, which would be
triggered by the first handler when the querying data store
is updated with the information about the case and the
agent. And have the second handler subscribe to this new
event rather than the original one.
But what if I have more than one event to notify about where I shouldn't have competing consumers? And the original event would need to be duplicated as-is as the same information would be required. I really don't want to do that. The good news is there's no need. Azure Service Bus is robust enough to allow message impersonation. How does it work?
The first handler, upon its completion, will dispatch a new
event. We'll use a convention of
{OriginalMessageType}Completed. In the case of
AgentReassigned, the newly dispatched event
will be AgentAssignedCompleted. But what we'll
do is stamp the new message headers with the
original message type and set the payload to the
original message payload.
var outgoingMessage = new ServiceBusMessage(BinaryData.FromObjectAsJson(message))
{
ApplicationProperties =
{
{ "EventType", $"{nameof(ConsultantReassigned)}Completed" },
{ "OriginalEventType", typeof(ConsultantReassigned).FullName }
}
};
await sender.SendMessageAsync(outgoingMessage);
The subscription we'll create for the 2nd handler will
subscribe to the AgentAssignedCompleted event
type, using SQL filter
EventType='ConsultantReassignedCompleted'. This
will ensure that copies of the messages of
ConsultantReassignedCompleted will be stored
under the subscription.
And here's the trick, we'll use SQL filter
actionto replace EventType of
the message that will be given to the subscription if it
matches the condition, back to the original message type
using the following instruction:
SET EventType=OriginalEventType; REMOVE
OriginalEventType;.
With this action, any message that has satisfied the SQL
filter will have its header EventType modified
to the header's value OriginalEventType,
removing the temporary OriginalEventType after
that.
When the 2nd handler receives messages from this
subscription, the type of the message indicated by
EventType will be the original
ConsultantReassigned event rather than the
modified ConsultantReassignedCompleted type.
And the payload will be the original
ConsultantReassigned payload.
Provisioning
There are several ways. Manually, using a tool such as ServiceBus Explorer, or scripted using Az CLI or Bicep. Bicep seems to have a bug, but Az CLI works great. This is what it would look like:
az servicebus topic subscription rule create --resource-group 'MyGroup' --namespace-name 'MyNamespace'
--topic-name 'tva.events' --subscription-name 'Notifications' --name ConsultantReassignedCompleted
--filter-sql-expression="EventType='ConsultantReassignedCompleted'"
--action-sql-expression='SET EventType=OriginalEventType; REMOVE OriginalEventType;'
Is this necessary?
It really depends. You could create Additional
xxxxCompleted types and duplicate all the
properties from the original message types if you'd like. We
can skip that and keep only the original events that matter,
enabling ordered processing by tweaking the provisioned
topology with event impersonation.