Accessing Service Bus message metadata in Azure Functions isolated model
In my recent post about migrating Azure Durable Functions to the isolated process model, I mentioned some limitations of the isolated model, particularly around Azure SDK support. In it I mentioned that I didn't think it was possible to access the metadata in a Service Bus message from within a Service Bus triggered function in the isolated process model. However, I have recently discovered that it is possible and in this post I will explain how.
Sending a Service Bus message with metadata
Let's start by sending a Service Bus message that includes some user-provided metadata. I do need to do this using Service Bus SDK directly rather than using Azure Function bindings due to their current limitations.
In this example, a simple HTTP triggered Azure Function will use an injected ServiceBusClient
from which it creates a ServiceBusSender
for the particular queue we are sending to. Then on the ServiceBusMessage
I can use ApplicationProperties
to attach arbitrary metadata to the message.
[Function(nameof(SendServiceBusMessage))]
public async Task<HttpResponseData> SendServiceBusMessage([HttpTrigger] HttpRequestData req)
{
_logger.LogInformation($"Sending a message");
var sender = serviceBusClient.CreateSender(QueueName);
var message = new ServiceBusMessage("Hello world");
message.ApplicationProperties["Sender"] = "Mark";
message.ApplicationProperties["Number"] = 12345;
await sender.SendMessageAsync(message);
await sender.DisposeAsync(); // n.b. a better option is to cache and reuse the sender
return req.CreateResponse(System.Net.HttpStatusCode.OK);
}
Note: the way I am making the ServiceBusClient
is using AddAzureClients
from the Microsoft.Extensions.Azure
NuGet I discussed in my previous posts.
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddAzureClients(clientBuilder =>
{
clientBuilder.AddServiceBusClient(
Environment.GetEnvironmentVariable("ServiceBus"));
});
})
.Build();
Receiving Service Bus messages
To receive Service Bus messages we use the ServiceBusTrigger
on our Azure Function, passing in the queue name we want to listen on, and the name of the Service Bus connection string. I'm just asking for the body as a string here, but it can also deserialize it into a strongly typed object for you.
However, the key to getting hold of message metadata is the FunctionContext
parameter.
[Function(nameof(ProcessServiceBusMessages))]
public void ProcessServiceBusMessages(
[ServiceBusTrigger(QueueName, Connection = "ServiceBus")] string queueBody,
FunctionContext context)
The FunctionContext
has a BindingContext
property which in turn has a string to object dictionary called BindingData
. If we want to explore what's in here, we could write some log messages out like this:
var bindingData = context.BindingContext.BindingData;
_logger.LogInformation($"Body: {queueBody} context: {string.Join(',', bindingData.Keys)}");
foreach(var key in bindingData.Keys)
{
_logger.LogInformation($"Key: {key}, Type: {bindingData[key]?.GetType()} Value: {bindingData[key]}");
}
What this reveals is that there is a lot of metadata in the message. This includes things like DeliveryCount
and the time at which the message was enqueued. But you'll also notice at the end of the list we have the ApplicationProperties
that we want. (it's actually in there twice - also as UserProperties
)
Key: MessageReceiver, Type: System.String Value: {}
Key: MessageSession, Type: System.String Value: {}
Key: MessageActions, Type: System.String Value: {}
Key: SessionActions, Type: System.String Value: {}
Key: Client, Type: System.String Value: {"FullyQualifiedNamespace":"REDACTED.servicebus.windows.net","IsClosed":false,"TransportType":0,"Identifier":"REDACTED.servicebus.windows.net-REDACTED"}
Key: ReceiveActions, Type: System.String Value: {}
Key: ExpiresAtUtc, Type: System.String Value: "2023-01-27T17:09:31.826"
Key: DeliveryCount, Type: System.String Value: 1
Key: ExpiresAt, Type: System.String Value: "2023-01-27T17:09:31.826+00:00"
Key: LockToken, Type: System.String Value: 2ebd4cbb-0150-4651-a9c9-949e623bd3d1
Key: EnqueuedTimeUtc, Type: System.String Value: "2023-01-13T17:09:31.826"
Key: EnqueuedTime, Type: System.String Value: "2023-01-13T17:09:31.826+00:00"
Key: SequenceNumber, Type: System.String Value: 9
Key: UserProperties, Type: System.String Value: {"Sender":"Mark","Number":12345}
Key: ApplicationProperties, Type: System.String Value: {"Sender":"Mark","Number":12345}
This means that we can deserialize the ApplicationProperties
JSON and get at the two metadata properties (Sender
and Number
) that we put on the original message. I've done that by deserializing into a Dictionary<string, object>
:
context.BindingContext.BindingData.TryGetValue("ApplicationProperties", out var appProperties);
if(appProperties is string properties)
{
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(properties);
if (dict != null)
{
_logger.LogInformation($"Sender: {dict["Sender"]}");
_logger.LogInformation($"Number: {dict["Number"]}");
}
}
By the way, if you're wondering how I found out that this was possible, it was thanks to this comment on GitHub, where
BindingContext
is called "Sean's property" (referring to Sean Feldman who is an expert in all things Azure Service Bus and has been heavily involved in the SDK design). Maybe the official docs cover this way of getting message metadata somewhere, but if they do, I'd certainly missed it.
Summary
Although it's not exactly obvious how to access ServiceBus message metadata (aka "Application Properties") in an isolated model Azure Functions app, it is possible, which at least removes one possible barrier to adopting isolating functions. The same approach can actually be used for Storage Queues, which don't have per-message user metadata, but does mean you can access useful information such as the dequeue count.