Azure Functions Isolated Worker - Sending multiple messages
The new Azure Functions SDK for Isolated Worker (process) has been introduced around .NET 5. While it's still in flux despite being GA-ed, it's gaining more and more popularity. And yet, there are still some sharp edges you should be careful with and validate that everything you're using with the older SDK, In-Process, is offered with the new SDK. Or at least there's a replacement.
Today, I've stumbled upon a StackOverflow question about
IAsyncCollector and Service Bus messages.
IAsyncCollector, as its synchronous counterpart
ICollector offers the comfort of output binding
and returning multiple items. For example, with Azure
Service Bus, one can send out multiple messages from the
executing function. Quite handy, and with the In-Process
SDK, it looks like the following. The function's signature
contains a collector (I call it dispatcher) that can be used
to "add" messages. Those are actually getting dispatched to
the queue the ServiceBus attribute is
configured with by adding messages. Which, in this case, is
a queue called dest.
[FunctionName("Concierge")]
public async Task<IActionResult> Handle([HttpTrigger(AuthorizationLevel.Function,"post", Route = "receive")] HttpRequest req,
[ServiceBus("dest", Connection = "AzureServiceBus")] IAsyncCollector<ServiceBusMessage> dispatcher)
And sending messages:
for (var i = 0; i < 10; i++)
{
var message = new ServiceBusMessage($"Message #{i}");
await collector.AddAsync(serviceBusMessage);
}
Straight forward and simple. But how do you do the same with the new Isolated Worker (out of process) SDK?
Not the same way. The new SDK doesn't currently support
native SDK types. Therefore types such as
ServiceBusMessage are not supported. Also, SDK
Service Bus clients are not available directly. So functions
need to marshal data as strings or byte arrays to be able to
send those. And receive as well. But we're focusing on
sending. So what's the way to send those multiple messages?
The official documentation does mention multiple output binding. But that's in the context of using multiple different output bindings. To output multiple items to the same output binding, we need to resort to a bit of tedious work.
First, we'll need to serialize our messages. Then we'll dispatch those serialized objects using an output binding, connected to a collection property. Here's an example:
[Function("OneToMany")]
public static DispatchedMessages Run([ServiceBusTrigger("myqueue",
Connection = "AzureServiceBus")] string myQueueItem, FunctionContext context)
{
// Generate 5 messages
var messages = new List<MyMessage>();
for (var i = 0; i < 5; i++)
{
var message = new MyMessage { Value = $"Message #{i}" };
messages.Add(message);
}
return new DispatchedMessages
{
Messages = messages.Select(x => JsonSerializer.Serialize(x))
};
}
Each message of type MyMessage is serialized
first.
class MyMessage
{
public string Value { get; set; }
}
And then, we return an object of
DispatchedMessage where the binding glue is:
public class DispatchedMessages
{
[ServiceBusOutput(queueOrTopicName: "dest", Connection = "AzureServiceBus")]
public IEnumerable<string> Messages { get; set; }
}
This object will be returned from the function and
marshalled back to the SDK code that will take care to
enumerate over the Messages property, taking
each string and passing it as the body value to the newly
constructed ServiceBusMessage. With the help of
the ServiceBusOutput attribute, Functions SDK
knows where to send the message and where to find the
connection string. Note that w/o specifying the connection
string name, the SDK will attempt to load the connection
string from the variable/key named
AzureWebJobsServiceBus. This means that we can
have multiple dispatchers, similar to the in-process SDK
multiple collectors, by having a property per
destination/namespace on the returned type.
And just like this, we can kick off the function and dispatch multiple messages with the new Isolated Worker SDK.