Posted in:

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.

Want to learn more about how easy it is to get up and running with Azure Functions? Be sure to check out my Pluralsight courses Azure Functions Fundamentals and Microsoft Azure Developer: Create Serverless Functions