skip to Main Content

I have a function app that we are upgrading from the In-Process Model to the Isolated Worker Model.
Below I have a dumbed-down, pseudocode version of what I am working on with the relevant method signature and call.

[FunctionName("DummyFunctionName")]
public async Task RunDummyFunction([TimerTrigger("%DummyTrigger%")] TimerInfo timerInfo,[ServiceBus("%DummyQueue%", EntityType = ServiceBusEntityType.Queue, Connection = "DummyConnectionString")] IAsyncCollector<string> queue)
{
    await _manager.DoStuff(queue);
}

We are passing the IAsyncCollector to the _manager.DoStuff. In that method, it will eventually call await queue.AddAsync(n1) where n1 is an IEnumerable string.

With the upgrades to the isolated worker model, I am unable get that IAsyncCollector anymore and pass it to the function.

I know I can decorate an object with the ServiceBus and return the object, but that is not going to return a Task in _manager.DoStuff. For example, we cannot do what it says in this article: https://weblogs.asp.net/sfeldman/functions-isolated-worker-sending-multiple-messages because it doesn’t return a Task.

This might be helpful too: https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-output?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-csharp#example

Please advise! Thank you!

2

Answers


  1. In Isolated Model you cannot use IAsyncCollector but you can use service bus clients to send messages as below:

    using System.Text.Json;
    using Azure.Messaging.ServiceBus;
    using Microsoft.Azure.Functions.Worker;
    using Microsoft.Extensions.Logging;
    
    namespace FunctionApp008
    {
        public class Func1
        {
            private readonly ILogger<Function1> ri_lg;
            private readonly ServiceBusClient ri_cl;
    
            public Function1(ILogger<Func1> logger, ServiceBusClient serviceBusClient)
            {
                ri_lg = logger;
                ri_cl = serviceBusClient;
            }
    
            [Function("Func1")]
            public async Task Run(
                [ServiceBusTrigger("testrith", Connection = "ritconst")]
                ServiceBusReceivedMessage message,
                ServiceBusMessageActions messageActions)
            {
                ri_lg.LogInformation("Message Body: {body}", message.Body);
                await test_rith_send();
                await messageActions.CompleteMessageAsync(message);
            }
    
            private async Task test_rith_send()
            {
                ServiceBusSender ri_sen = ri_cl.CreateSender("testrith");
                List<ServiceBusMessage> rimsg = new List<ServiceBusMessage>
                {
                    new ServiceBusMessage(JsonSerializer.Serialize(new { Value = "Rithwik Msg 1" })),
                    new ServiceBusMessage(JsonSerializer.Serialize(new { Value = "Rithwik Msg 2" }))
                };
                await ri_sen.SendMessagesAsync(rimsg);
            }
        }
    }
    

    Output:

    enter image description here

    Login or Signup to reply.
  2. Summary

    (Following on from my comment)

    Seeing as you already have a Service class, I’d be tempted to ditch the Function bindings entirely and instead use a Repository pattery. So, make the service take a repository interface representing the queue output, and then inject an implementation based on Service Bus. Here’s how.

    1. Create an abstraction for Service Bus

    Ignore that it’s Service Bus for now, and just create your own interface that represents a queue. Maybe put the interface in the same project as your Service.

    namespace DummyApp.Services;
    
    public interface IQueue
    {
        Task Enqueue(string toQueue);
    }
    

    (this will be useful if you want to Unit Test your Manager class too, as we’ll see later.)

    2. Change your Manager class to use the IQueue abstraction

    Your Manager class now takes an IQueue object in the Constructor, and the DoStuff method no longer requires any parameters:

    namespace DummyApp.Services;
    
    public class Manager(IQueue _queue)
    {
        public async Task DoStuff()
        {
            string[] thingsToEnqueue = ["this", "that"]; // Or wherever they come from.
    
            foreach (var thing in thingsToEnqueue)
            {
                // Call the IQueue method for each thing you send.
                // (NB if you want to do this as a batch, I'll show that later)
                await _queue.Enqueue(thing);
            }
        }
    }
    

    3. Write an IQueue implementation that uses Service Bus

    This takes a Constructor parameter of ServiceBusClient (which we will wire-up in the Dependency Injection config later.)

    using Azure.Messaging.ServiceBus;
    using DummyApp.Services;
    
    namespace DummyApp.ServiceBus;
    
    public class ServiceBusQueue(ServiceBusClient _client) : IQueue
    {
        public async Task Enqueue(string toQueue)
        {
            var sender = _client.CreateSender("DummyQueue");
            var message = new ServiceBusMessage(toQueue);
    
            await sender.SendMessageAsync(message);
        }
    }
    

    Obviously you’ll want to pass through any config etc for the queue names.

    Note, you might want to create a new Project to put this implementation in, so that you can keep the Service Bus dependencies separate from your Services or your Function App.

    4. Wire up the above classes in Dependency Injection

    This is for a .NET 8 Isolated Function using the latest (at time of recording…) ASP.NET Core style of Function App. Merge this with any other Dependency Injection config that you need to do.

    using Microsoft.Extensions.Azure;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using DummyApp.ServiceBus;
    using DummyApp.Services;
    
    var host = new HostBuilder()
        .ConfigureFunctionsWebApplication()
        .ConfigureServices(services =>
        {
            services.AddAzureClients(configureClients =>
            {
                configureClients.AddServiceBusClient("DummyConnectionString");
            });
    
            services.AddScoped<IQueue, ServiceBusQueue>();
            services.AddScoped<Manager>();
        })
        .Build();
    
    host.Run();
    

    Other bonuses

    Unit Testing

    Because your Manager class doesn’t need any Service Bus (or Azure Functions) specific types, you can Unit Test the behaviour by just mocking the IQueue implementation with something else.

    Batch queueing

    If you want to send all your items at once, you can just modify or extend the IQueue implementation accordingly. For example:

    public interface IQueue
    {
        Task EnqueueAsBatch(string[] toQueue);
    }
    

    and:

    public class ServiceBusQueue(ServiceBusClient _client) : IQueue
    {
        public async Task EnqueueAsBatch(string[] toQueue)
        {
            var sender = _client.CreateSender("DummyQueue");
            var batch = await sender.CreateMessageBatchAsync();
    
            foreach (string item in toQueue)
            {
                batch.TryAddMessage(new ServiceBusMessage(item));
            }
    
            await sender.SendMessagesAsync(batch);
        }
    }
    

    and change your Manager class accordingly:

    public class Manager(IQueue _queue)
    {
        public async Task DoStuff()
        {
            string[] thingsToEnqueue = ["this", "that"];
    
            await _queue.EnqueueAsBatch(thingsToEnqueue);
        }
    }
    

    Hope that’s helpful!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search