azuredependency-injectionazure-web-app-serviceazureservicebus

Is using AddAzureClients and a clientFactory for Azure ServiceBus DI preferred over a singleton implementation?


I've got two different implementations of a service class that we use from our web-app to talk to Azure Service bus. They both work.

One is what I would consider to be a standard singleton service class that instantiates and disposes up the ServiceBus clients and senders and is added as a singleton using .net DI on startup.

The other is where I've recently been playing with the AzureAddClients DI method to configure the ServiceBus clients and senders as part of the startup code rather than having that logic encapsulated in the service class. The service class takes in IAzureCLientFactory to trigger the lazy instantiation of the senders etc when needed and allows the service class to be scoped rather than singleton if needed (although it feels like the service class still would work well as a singleton?).

This second approach seems to be what is recommended as the way to go in the latest microsoft docs but either wasn't around (or I missed it) when I wrote the first implementation.

Is there any advantage/best practice reasons of one approach over the other? Code for the two different approaches below for clarity.

"Standard singleton approach"

startup code

 services.AddSingleton<IAIMarkingMessageService>(s => new AIMarkingMessageServiceSingleton(aiMarkingSettings.ServiceBusNamespace, aiMarkingSettings.InitQueueName, aiMarkingSettings.FeedbackQueueName, siteSettings.DefaultManagedIdentityClientId));

service class code/outline (some details removed for brevity)

public class AIMarkingMessageServiceSingleton : IAIMarkingMessageService, IAsyncDisposable
{
    readonly ServiceBusClient _client;
    readonly ServiceBusSender _initSender;
    readonly ServiceBusSender _feedbackSender;

    public AIMarkingMessageServiceSingleton(string serviceBusNameSpace, string initQueueName, string feedbackQueueName, string userAssignedClientId)
    {
        var clientOptions = new ServiceBusClientOptions { TransportType = ServiceBusTransportType.AmqpWebSockets };
        _client = new ServiceBusClient($"{serviceBusNameSpace}.servicebus.windows.net", new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = userAssignedClientId }), clientOptions);
        _initSender = _client.CreateSender(initQueueName);
        _feedbackSender = _client.CreateSender(feedbackQueueName);
    }

    public async Task<string> SendInitSubmissionAsync(long taskId, string requestUID)
    { 
       ...
    }

    public async Task<List<long>> SendInitSubmissionBatchAsync(IEnumerable<(long taskId, string requestUID)> tasksToInit)
    {
         ...
    }

    public async ValueTask DisposeAsync()
    {         
        await _client.DisposeAsync(); // senders and receivers created by this client will also be disposed by this call
        GC.SuppressFinalize(this);
    }
}

"AddAzureClients approach"

startup code

services.AddAzureClients(clientBuilder =>
{
    // register the clients
    clientBuilder.AddServiceBusClientWithNamespace($"{aiMarkingSettings.ServiceBusNamespace}.servicebus.windows.net")
                 .ConfigureOptions(x => new ServiceBusClientOptions { TransportType = ServiceBusTransportType.AmqpTcp });

    clientBuilder.UseCredential(new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = siteSettings.DefaultManagedIdentityClientId }));

    // register the sub clients (queue senders)
    // init sender
    clientBuilder.AddClient<ServiceBusSender, ServiceBusClientOptions>(

        (_, _, provider) => provider.GetService<ServiceBusClient>()
        .CreateSender(aiMarkingSettings.InitQueueName)).WithName(aiMarkingSettings.InitQueueName);
    // feedback sender
    clientBuilder.AddClient<ServiceBusSender, ServiceBusClientOptions>(
        (_, _, provider) => provider.GetService<ServiceBusClient>()
        .CreateSender(aiMarkingSettings.FeedbackQueueName)).WithName(aiMarkingSettings.FeedbackQueueName);
});

// could/should be singleton rather than scoped?
services.AddScoped<IAIMarkingMessageService, AIMarkingMessageService>();

service class code/outline

public class AIMarkingMessageService : IAIMarkingMessageService
{
    readonly ServiceBusSender _initSender;
    readonly ServiceBusSender _feedbackSender;

    public AIMarkingMessageService(IAzureClientFactory<ServiceBusSender> senderFactory, IOptions<AIMarkingSettings> config )
    {
        var _config = config.Value;
        _initSender = senderFactory.CreateClient(_config.InitQueueName);
        _feedbackSender = senderFactory.CreateClient(_config.FeedbackQueueName);
    }
    
    public async Task<string> SendInitSubmissionAsync(long taskId, string requestUID)
    { 
       ...
    }

    public async Task<List<long>> SendInitSubmissionBatchAsync(IEnumerable<(long taskId, string requestUID)> tasksToInit)
    {
         ...
    }
}

Solution

  • For this implementation, there's no reason to prefer one over the other. It comes down to preference for what more naturally fits the patterns of the application.

    Both approaches are equivalent in that they take care of the important parts: