Leaders Logo

Application of best practices in developing workers in .NET for consuming messages in Azure Service Bus.

Introduction to Azure Service Bus

The Azure Service Bus is a messaging solution offered by Microsoft Azure that enables reliable and scalable communication between different components of an application (PIISPANEN, 2022). It supports messaging patterns such as queues and topics, allowing messages to be sent and received asynchronously. This is especially useful in distributed architectures, where different components of the application may be on different servers or even in different geographical regions.

With Azure Service Bus, you can decouple components, allowing them to communicate efficiently and securely (TEIXEIRA, 2020). In this article, we will discuss how to apply best practices in developing .NET workers that consume messages from Azure Service Bus, ensuring efficiency, maintenance, and scalability in your applications.

Setting Up the Environment

To start working with Azure Service Bus in .NET, you will need to set up your environment. First, install the necessary NuGet package to interact with Azure Service Bus. Run the following command in your NuGet console:

Install-Package Azure.Messaging.ServiceBus

Then, create an instance of the Service Bus in the Azure portal and obtain the connection string. The connection string contains essential information such as the namespace and the credentials needed to access Azure Service Bus.

Basic Worker Structure

A worker is a background service that processes messages. Below is an example of how you can create a simple worker that consumes messages from Azure Service Bus:

using Azure.Messaging.ServiceBus;
using System;
using System.Threading.Tasks;

namespace WorkerService
{
    public class MessageProcessor
    {
        private readonly ServiceBusClient _client;
        private readonly ServiceBusProcessor _processor;

        public MessageProcessor(string connectionString, string queueName)
        {
            _client = new ServiceBusClient(connectionString);
            _processor = _client.CreateProcessor(queueName, new ServiceBusProcessorOptions());
        }

        public async Task StartProcessingAsync()
        {
            _processor.ProcessMessageAsync += MessageHandler;
            _processor.ProcessErrorAsync += ErrorHandler;

            await _processor.StartProcessingAsync();
            Console.WriteLine("Press any key to stop...");
            Console.ReadKey();
            await _processor.StopProcessingAsync();
        }

        private async Task MessageHandler(ProcessMessageEventArgs args)
        {
            string body = args.Message.Body.ToString();
            Console.WriteLine($"Message received: {body}");
            await args.CompleteMessageAsync(args.Message);
        }

        private Task ErrorHandler(ProcessErrorEventArgs args)
        {
            Console.WriteLine($"Error: {args.Exception.Message}");
            return Task.CompletedTask;
        }
    }
}

Connection Management

One of the best practices when working with Azure Service Bus is to manage connections efficiently. It is important to avoid creating new instances of ServiceBusClient for each operation. Instead, create a single instance and reuse it throughout the application's lifecycle. This not only improves performance but also reduces resource consumption.

private static ServiceBusClient _client;

public static void Initialize(string connectionString)
{
    _client = new ServiceBusClient(connectionString);
}

Settings and Retries

Configuring ASB is important to ensure that messages are not lost in case of temporary failures. Azure Service Bus supports automatic retries. You can configure processing options to handle errors more granularly. By implementing retry logic, you can set a maximum number of attempts and a wait strategy between attempts, ensuring that your worker continues to function even in adverse situations.

var options = new ServiceBusProcessorOptions
{
    MaxConcurrentCalls = 10,
    MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5),
    PrefetchCount = 100
};

In this example:

  • The client can process up to 10 messages in parallel (MaxConcurrentCalls);
  • It automatically renews the message lock for up to 5 minutes;
  • It prefetches 100 messages – meaning it already loads 100 messages from the queue, anticipating consumption to optimize performance.

Scalability and Performance

When designing your worker, consider scalability. You can increase the number of running worker instances to handle demand spikes. Azure Service Bus allows you to scale horizontally by increasing the number of running worker instances. Use MaxConcurrentCalls to balance the message load among instances (REAGAN, 2017), which helps maximize performance. Additionally, monitor application performance to identify bottlenecks and optimize the configuration as needed.

Monitoring and Logging

Application Insights

Implementing a good monitoring and logging system is essential. Use tools like Application Insights to track your worker's performance and capture exceptions. Azure offers integration with Application Insights, making it easy to collect data on application performance, such as response times and error rates. This will allow you to quickly identify issues and take corrective actions.

private static void ConfigureLogging()
{
    var loggerFactory = LoggerFactory.Create(builder =>
    {
        builder.AddApplicationInsights("", LogLevel.Information);
    });
    Logger = loggerFactory.CreateLogger("WorkerService");
}

Datadog

Datadog is a powerful monitoring and observability platform that allows you to collect metrics, logs, and distributed traces from your application. It integrates well with cloud services, containers, and distributed architectures. Below is an example of how to configure Datadog in a .NET application to send logs:

public static void ConfigureDatadogLogging()
{
    var loggerFactory = LoggerFactory.Create(builder =>
    {
        builder.AddDatadogLogs(options =>
        {
            options.ApiKey = "<YOUR_DATADOG_API_KEY>";
            options.Service = "WorkerService";
            options.Hostname = Environment.MachineName;
            options.Source = "csharp";
            options.Tags = new[] { "env:production", "team:backend" };
        });
    });

    Logger = loggerFactory.CreateLogger("WorkerService");
}

You can also send custom metrics directly to Datadog using the DogStatsD client. Below is an example where we log the count of messages received from an Azure Service Bus queue:

using StatsdClient;

public class MessageProcessor
{
    private static readonly Statsd statsd = new Statsd("127.0.0.1", 8125); // Address of the Datadog agent

    public async Task ProcessMessageAsync(ServiceBusReceivedMessage message)
    {
        try
        {
            // Process the message
            // ...

            // Send a custom metric to Datadog
            statsd.Increment("azure_servicebus.messages_received", tags: new[] { "queue:orders", "env:production" });

            Logger.LogInformation("Message processed successfully.");
        }
        catch (Exception ex)
        {
            statsd.Increment("azure_servicebus.processing_errors", tags: new[] { "queue:orders", "env:production" });
            Logger.LogError(ex, "Error processing message.");
        }
    }
}

These metrics can be viewed in Datadog dashboards, and you can set up alerts for situations such as an increase in message processing failures or variations in the received volume.

Conclusion

Creating workers in .NET to consume messages from Azure Service Bus is a task that can be optimized by applying best practices. By managing connections effectively, handling errors appropriately, monitoring performance, and ensuring security in communication, you can build robust and scalable systems that meet the needs of your application.

References

  • TEIXEIRA, Bruna Moreira. Message-based architectures. Master's Thesis. Polytechnic Institute of Porto (Portugal). 2020. reference.Description
  • PIISPANEN, Juho. Microsoft Azure as an Integration Platform. 2022. reference.Description
  • REAGAN, Rob. Message Queues. In: Web Applications on Azure: Developing for Global Scale. Berkeley, CA: Apress, 2017. p. 343-380. reference.Description
About the author