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.
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);
}
}
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)
{
...
}
}
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:
There is a single instance of the ServiceBusClient
and each sender which are reused.
The lifetime of the ServiceBusClient
is managed by DI and it is properly disposed.