Event-Driven Architecture with .NET and Azure Service Bus

Tapesh Mehta Tapesh Mehta | Published on: Feb 13, 2026 | Est. reading time: 12 minutes
Event-Driven Architecture with .NET and Azure Service Bus

Modern software applications increasingly demand scalability, resilience, and the ability to handle asynchronous communication between distributed components. Event-driven architecture (EDA) has emerged as a powerful pattern to address these challenges, and when combined with Azure Service Bus and .NET, it provides a robust foundation for building enterprise-grade applications. This comprehensive guide explores how to implement event-driven architecture using Azure Service Bus in .NET applications, complete with practical code examples and production-ready patterns.

Table of Contents

Understanding Event-Driven Architecture

Event-driven architecture is a software design pattern where the flow of the program is determined by events such as user actions, sensor outputs, or messages from other programs. Instead of components directly calling each other, they communicate by producing and consuming events. This decoupling enables better scalability, flexibility, and maintainability in complex distributed systems.

When implementing microservices architectures, event-driven patterns become essential for managing inter-service communication without creating tight coupling. Azure Service Bus serves as the messaging backbone that facilitates this communication pattern in cloud-native applications.

Why Azure Service Bus for Event-Driven Architecture?

Azure Service Bus is a fully managed enterprise message broker with message queues and publish-subscribe topics. Unlike simpler messaging systems, Azure Service Bus provides advanced features like message sessions, dead-letter queues, duplicate detection, and transaction support that are crucial for production scenarios.

Key Benefits for .NET Applications

Azure Service Bus integrates seamlessly with .NET through the Azure.Messaging.ServiceBus SDK, offering robust reliability guarantees including at-least-once delivery, message ordering, and automatic retry mechanisms. The platform handles infrastructure concerns like high availability, disaster recovery, and automatic scaling, allowing developers to focus on business logic rather than messaging infrastructure.

Setting Up Azure Service Bus in .NET

Before implementing event-driven patterns, you need to set up Azure Service Bus and configure your .NET application. Start by creating a Service Bus namespace in the Azure portal and installing the required NuGet package.

Installing Dependencies

First, install the Azure Service Bus client library in your .NET project:

dotnet add package Azure.Messaging.ServiceBus
dotnet add package Microsoft.Extensions.Azure
dotnet add package Microsoft.Extensions.Configuration

Configuration Setup

Configure the Service Bus connection in your appsettings.json file:

{
  "AzureServiceBus": {
    "ConnectionString": "Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key",
    "QueueName": "orders-queue",
    "TopicName": "events-topic",
    "SubscriptionName": "notification-subscription"
  }
}

Implementing Message Publishing

Publishing messages to Azure Service Bus is the foundation of event-driven architecture with Azure Service Bus. Let’s create a publisher service that sends events to a queue or topic.

Creating the Event Publisher

Here’s a complete implementation of an event publisher service:

using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Configuration;
using System.Text.Json;

public interface IEventPublisher
{
    Task PublishAsync<T>(T eventData, string queueOrTopicName);
}

public class ServiceBusEventPublisher : IEventPublisher
{
    private readonly ServiceBusClient _client;
    private readonly IConfiguration _configuration;

    public ServiceBusEventPublisher(IConfiguration configuration)
    {
        _configuration = configuration;
        var connectionString = _configuration["AzureServiceBus:ConnectionString"];
        _client = new ServiceBusClient(connectionString);
    }

    public async Task PublishAsync<T>(T eventData, string queueOrTopicName)
    {
        await using ServiceBusSender sender = _client.CreateSender(queueOrTopicName);

        try
        {
            // Serialize the event data
            var messageBody = JsonSerializer.Serialize(eventData);
            var message = new ServiceBusMessage(messageBody)
            {
                ContentType = "application/json",
                MessageId = Guid.NewGuid().ToString(),
                CorrelationId = Guid.NewGuid().ToString()
            };

            // Add custom properties for message routing and filtering
            message.ApplicationProperties.Add("EventType", typeof(T).Name);
            message.ApplicationProperties.Add("PublishedAt", DateTime.UtcNow);

            await sender.SendMessageAsync(message);
            Console.WriteLine($"Published event {typeof(T).Name} to {queueOrTopicName}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error publishing event: {ex.Message}");
            throw;
        }
    }
}

Publishing Domain Events

Define your domain events as POCOs (Plain Old CLR Objects):

public class OrderCreatedEvent
{
    public string OrderId { get; set; }
    public string CustomerId { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class OrderItem
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

// Usage in your service
public class OrderService
{
    private readonly IEventPublisher _eventPublisher;

    public OrderService(IEventPublisher eventPublisher)
    {
        _eventPublisher = eventPublisher;
    }

    public async Task CreateOrderAsync(Order order)
    {
        // Save order to database
        // ... database logic ...

        // Publish event
        var orderEvent = new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId,
            TotalAmount = order.Total,
            CreatedAt = DateTime.UtcNow,
            Items = order.Items.Select(i => new OrderItem 
            { 
                ProductId = i.ProductId, 
                Quantity = i.Quantity, 
                Price = i.Price 
            }).ToList()
        };

        await _eventPublisher.PublishAsync(orderEvent, "orders-topic");
    }
}

Implementing Message Consumption

Consuming messages from Azure Service Bus requires setting up processors that handle incoming events. Similar to patterns used in event-driven architectures with message brokers, we need reliable message processing with error handling.

Creating the Event Consumer

using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System.Text.Json;

public class ServiceBusEventConsumer : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    private readonly IConfiguration _configuration;
    private readonly IServiceProvider _serviceProvider;

    public ServiceBusEventConsumer(
        IConfiguration configuration,
        IServiceProvider serviceProvider)
    {
        _configuration = configuration;
        _serviceProvider = serviceProvider;

        var connectionString = _configuration["AzureServiceBus:ConnectionString"];
        var queueName = _configuration["AzureServiceBus:QueueName"];

        var client = new ServiceBusClient(connectionString);
        
        var processorOptions = new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 5,
            AutoCompleteMessages = false,
            PrefetchCount = 10,
            ReceiveMode = ServiceBusReceiveMode.PeekLock
        };

        _processor = client.CreateProcessor(queueName, processorOptions);
        _processor.ProcessMessageAsync += MessageHandler;
        _processor.ProcessErrorAsync += ErrorHandler;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _processor.StartProcessingAsync(stoppingToken);
        
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }

        await _processor.StopProcessingAsync();
    }

    private async Task MessageHandler(ProcessMessageEventArgs args)
    {
        try
        {
            var body = args.Message.Body.ToString();
            var eventType = args.Message.ApplicationProperties["EventType"].ToString();

            Console.WriteLine($"Processing {eventType}: {body}");

            // Route to appropriate handler based on event type
            switch (eventType)
            {
                case nameof(OrderCreatedEvent):
                    var orderEvent = JsonSerializer.Deserialize<OrderCreatedEvent>(body);
                    await HandleOrderCreatedEvent(orderEvent);
                    break;
                default:
                    Console.WriteLine($"Unknown event type: {eventType}");
                    break;
            }

            // Complete the message
            await args.CompleteMessageAsync(args.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error processing message: {ex.Message}");
            // Move to dead-letter queue after max retry attempts
            await args.DeadLetterMessageAsync(args.Message, "ProcessingError", ex.Message);
        }
    }

    private async Task HandleOrderCreatedEvent(OrderCreatedEvent orderEvent)
    {
        // Process the order created event
        // Example: Send confirmation email, update inventory, etc.
        Console.WriteLine($"Order {orderEvent.OrderId} created for customer {orderEvent.CustomerId}");
        await Task.CompletedTask;
    }

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

    public override void Dispose()
    {
        _processor?.DisposeAsync().GetAwaiter().GetResult();
        base.Dispose();
    }
}

Implementing Publish-Subscribe Pattern

The publish-subscribe pattern allows multiple subscribers to receive the same event. This is particularly useful when different services need to react to the same event in different ways.

Topic and Subscription Configuration

Create a topic subscriber that can filter messages based on custom properties:

public class TopicSubscriberService : BackgroundService
{
    private readonly ServiceBusProcessor _processor;

    public TopicSubscriberService(IConfiguration configuration)
    {
        var connectionString = configuration["AzureServiceBus:ConnectionString"];
        var topicName = configuration["AzureServiceBus:TopicName"];
        var subscriptionName = configuration["AzureServiceBus:SubscriptionName"];

        var client = new ServiceBusClient(connectionString);
        
        var processorOptions = new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 3,
            AutoCompleteMessages = false
        };

        _processor = client.CreateProcessor(topicName, subscriptionName, processorOptions);
        _processor.ProcessMessageAsync += ProcessMessageAsync;
        _processor.ProcessErrorAsync += ErrorHandlerAsync;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _processor.StartProcessingAsync(stoppingToken);
        
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
    {
        var message = args.Message;
        var body = message.Body.ToString();

        Console.WriteLine($"Received from subscription: {body}");

        // Process based on message properties
        if (message.ApplicationProperties.ContainsKey("Priority") &&
            message.ApplicationProperties["Priority"].ToString() == "High")
        {
            // Handle high priority messages
            await ProcessHighPriorityMessage(body);
        }
        else
        {
            // Handle normal messages
            await ProcessNormalMessage(body);
        }

        await args.CompleteMessageAsync(message);
    }

    private Task ProcessHighPriorityMessage(string messageBody)
    {
        Console.WriteLine($"Processing high priority: {messageBody}");
        return Task.CompletedTask;
    }

    private Task ProcessNormalMessage(string messageBody)
    {
        Console.WriteLine($"Processing normal priority: {messageBody}");
        return Task.CompletedTask;
    }

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

Advanced Patterns and Best Practices

When building production-ready event-driven systems, implementing advanced patterns ensures reliability and maintainability. These patterns complement architectural approaches discussed in CQRS and event sourcing implementations.

Message Retry and Dead-Letter Handling

public class ResilientMessageProcessor
{
    private const int MaxRetryAttempts = 3;

    public async Task ProcessWithRetryAsync(
        ProcessMessageEventArgs args,
        Func<string, Task> processAction)
    {
        var message = args.Message;
        var deliveryCount = message.DeliveryCount;

        try
        {
            var body = message.Body.ToString();
            await processAction(body);
            await args.CompleteMessageAsync(message);
        }
        catch (Exception ex)
        {
            if (deliveryCount >= MaxRetryAttempts)
            {
                // Maximum retries exceeded, move to dead-letter queue
                var deadLetterReason = "MaxRetriesExceeded";
                var deadLetterDescription = $"Failed after {MaxRetryAttempts} attempts: {ex.Message}";
                
                await args.DeadLetterMessageAsync(
                    message, 
                    deadLetterReason, 
                    deadLetterDescription);
                
                Console.WriteLine($"Message moved to dead-letter queue: {deadLetterReason}");
            }
            else
            {
                // Abandon message to retry
                await args.AbandonMessageAsync(message);
                Console.WriteLine($"Message abandoned for retry. Attempt {deliveryCount + 1}");
            }
        }
    }
}

// Dead-letter queue processor
public class DeadLetterQueueProcessor
{
    public async Task ProcessDeadLetterMessagesAsync(string queueName)
    {
        var connectionString = "your-connection-string";
        var client = new ServiceBusClient(connectionString);
        
        var receiver = client.CreateReceiver(
            queueName, 
            new ServiceBusReceiverOptions 
            { 
                SubQueue = SubQueue.DeadLetter 
            });

        while (true)
        {
            var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
            
            if (message == null)
                break;

            Console.WriteLine($"Dead-letter message: {message.Body}");
            Console.WriteLine($"Reason: {message.DeadLetterReason}");
            Console.WriteLine($"Description: {message.DeadLetterErrorDescription}");

            // Log for investigation or reprocess
            await receiver.CompleteMessageAsync(message);
        }

        await receiver.DisposeAsync();
    }
}

Message Deduplication

Azure Service Bus provides built-in duplicate detection, but you can also implement application-level deduplication:

public class DeduplicationService
{
    private readonly IDistributedCache _cache;
    private const int DeduplicationWindowMinutes = 60;

    public DeduplicationService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<bool> IsDuplicateAsync(string messageId)
    {
        var cacheKey = $"msg:{messageId}";
        var cachedValue = await _cache.GetStringAsync(cacheKey);

        if (cachedValue != null)
        {
            return true; // Duplicate detected
        }

        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(DeduplicationWindowMinutes)
        };

        await _cache.SetStringAsync(cacheKey, DateTime.UtcNow.ToString(), options);
        return false;
    }
}

// Usage in message handler
private async Task ProcessMessageWithDeduplication(ProcessMessageEventArgs args)
{
    var messageId = args.Message.MessageId;

    if (await _deduplicationService.IsDuplicateAsync(messageId))
    {
        Console.WriteLine($"Duplicate message detected: {messageId}");
        await args.CompleteMessageAsync(args.Message);
        return;
    }

    // Process the message
    await ProcessMessageAsync(args);
}

Registering Services in Dependency Injection

Properly configuring dependency injection ensures your event-driven components work seamlessly with the rest of your application:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container
        builder.Services.AddSingleton<IEventPublisher, ServiceBusEventPublisher>();
        builder.Services.AddHostedService<ServiceBusEventConsumer>();
        builder.Services.AddHostedService<TopicSubscriberService>();
        
        // Add distributed cache for deduplication
        builder.Services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = builder.Configuration.GetConnectionString("Redis");
        });
        
        builder.Services.AddSingleton<DeduplicationService>();
        builder.Services.AddSingleton<ResilientMessageProcessor>();

        var app = builder.Build();
        app.Run();
    }
}

Monitoring and Observability

Implementing proper monitoring is crucial for production event-driven systems. Integrate Azure Application Insights for comprehensive telemetry:

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

public class MonitoredEventPublisher : IEventPublisher
{
    private readonly ServiceBusClient _client;
    private readonly TelemetryClient _telemetry;

    public MonitoredEventPublisher(
        IConfiguration configuration,
        TelemetryClient telemetry)
    {
        var connectionString = configuration["AzureServiceBus:ConnectionString"];
        _client = new ServiceBusClient(connectionString);
        _telemetry = telemetry;
    }

    public async Task PublishAsync<T>(T eventData, string queueOrTopicName)
    {
        var operation = _telemetry.StartOperation<DependencyTelemetry>("ServiceBus Publish");
        operation.Telemetry.Type = "Azure Service Bus";
        operation.Telemetry.Target = queueOrTopicName;

        try
        {
            await using ServiceBusSender sender = _client.CreateSender(queueOrTopicName);
            
            var messageBody = JsonSerializer.Serialize(eventData);
            var message = new ServiceBusMessage(messageBody)
            {
                MessageId = Guid.NewGuid().ToString(),
                ContentType = "application/json"
            };

            message.ApplicationProperties.Add("EventType", typeof(T).Name);

            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
            await sender.SendMessageAsync(message);
            stopwatch.Stop();

            operation.Telemetry.Duration = stopwatch.Elapsed;
            operation.Telemetry.Success = true;

            _telemetry.TrackEvent("EventPublished", new Dictionary<string, string>
            {
                { "EventType", typeof(T).Name },
                { "Destination", queueOrTopicName },
                { "MessageId", message.MessageId }
            });
        }
        catch (Exception ex)
        {
            operation.Telemetry.Success = false;
            _telemetry.TrackException(ex);
            throw;
        }
        finally
        {
            _telemetry.StopOperation(operation);
        }
    }
}

Integration with Serverless Functions

Event-driven architecture with Azure Service Bus integrates seamlessly with serverless computing patterns using Azure Functions, allowing you to build highly scalable event processors:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class ServiceBusFunction
{
    private readonly ILogger<ServiceBusFunction> _logger;

    public ServiceBusFunction(ILogger<ServiceBusFunction> logger)
    {
        _logger = logger;
    }

    [Function("OrderProcessor")]
    public async Task Run(
        [ServiceBusTrigger("orders-queue", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions)
    {
        _logger.LogInformation("Processing message: {MessageId}", message.MessageId);

        try
        {
            var orderEvent = JsonSerializer.Deserialize<OrderCreatedEvent>(message.Body.ToString());
            
            // Process the order
            await ProcessOrderAsync(orderEvent);

            // Complete the message
            await messageActions.CompleteMessageAsync(message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing message {MessageId}", message.MessageId);
            
            // Move to dead-letter queue
            await messageActions.DeadLetterMessageAsync(message);
        }
    }

    private async Task ProcessOrderAsync(OrderCreatedEvent orderEvent)
    {
        _logger.LogInformation("Processing order {OrderId}", orderEvent.OrderId);
        // Business logic here
        await Task.CompletedTask;
    }
}

Security Best Practices

Security is paramount when implementing event-driven architecture with Azure Service Bus. Never hardcode connection strings in your application code. Instead, use Azure Key Vault to store sensitive configuration securely and retrieve it at runtime. You can leverage professional .NET development services to ensure your application follows enterprise security standards.

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

public class SecureServiceBusConfiguration
{
    public static async Task<string> GetConnectionStringFromKeyVaultAsync()
    {
        var keyVaultUrl = "https://your-keyvault.vault.azure.net/";
        var client = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
        
        KeyVaultSecret secret = await client.GetSecretAsync("ServiceBusConnectionString");
        return secret.Value;
    }
}

// Usage in Startup
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = SecureServiceBusConfiguration
            .GetConnectionStringFromKeyVaultAsync()
            .GetAwaiter()
            .GetResult();

        services.AddSingleton<ServiceBusClient>(_ => new ServiceBusClient(connectionString));
    }
}

Performance Optimization

Optimizing message processing performance involves several strategies including batching, prefetching, and parallel processing:

public class OptimizedMessageBatchProcessor
{
    private readonly ServiceBusClient _client;

    public async Task ProcessBatchAsync(string queueName)
    {
        await using ServiceBusReceiver receiver = _client.CreateReceiver(queueName);

        while (true)
        {
            // Receive batch of messages
            var messages = await receiver.ReceiveMessagesAsync(
                maxMessages: 100,
                maxWaitTime: TimeSpan.FromSeconds(5));

            if (!messages.Any())
                break;

            // Process messages in parallel
            var tasks = messages.Select(async message =>
            {
                try
                {
                    await ProcessSingleMessageAsync(message.Body.ToString());
                    await receiver.CompleteMessageAsync(message);
                }
                catch (Exception ex)
                {
                    await receiver.AbandonMessageAsync(message);
                    Console.WriteLine($"Error: {ex.Message}");
                }
            });

            await Task.WhenAll(tasks);
        }
    }

    private async Task ProcessSingleMessageAsync(string messageBody)
    {
        // Process message
        await Task.Delay(100); // Simulate processing
    }
}

Conclusion

Event-driven architecture with Azure Service Bus and .NET provides a robust foundation for building scalable, distributed applications. By implementing proper message publishing and consumption patterns, handling errors gracefully, and following best practices for monitoring and security, you can create production-ready systems that handle millions of events reliably.

The patterns demonstrated in this guide form the cornerstone of modern cloud-native application development. Whether you’re building microservices, implementing real-time data processing, or creating loosely coupled systems, Azure Service Bus combined with .NET offers the enterprise-grade messaging infrastructure needed for success. For complex enterprise implementations, consider partnering with experienced teams that specialize in ASP.NET development to ensure your event-driven architecture meets production standards.

Start implementing these patterns in your applications today, and leverage the power of event-driven architecture to build more resilient, scalable, and maintainable systems.

Share

clutch profile designrush wirefuture profile goodfirms wirefuture profile
Join the Digital Revolution with WireFuture! 🌟

Step into the future with WireFuture at your side. Our developers harness the latest technologies to deliver solutions that are agile, robust, and ready to make an impact in the digital world.

Hire Now

Categories
.NET Development Angular Development JavaScript Development KnockoutJS Development NodeJS Development PHP Development Python Development React Development Software Development SQL Server Development VueJS Development All
About Author
wirefuture - founder

Tapesh Mehta

verified Verified
Expert in Software Development

Tapesh Mehta is a seasoned tech worker who has been making apps for the web, mobile devices, and desktop for over 15+ years. Tapesh knows a lot of different computer languages and frameworks. For robust web solutions, he is an expert in Asp.Net, PHP, and Python. He is also very good at making hybrid mobile apps, which use Ionic, Xamarin, and Flutter to make cross-platform user experiences that work well together. In addition, Tapesh has a lot of experience making complex desktop apps with WPF, which shows how flexible and creative he is when it comes to making software. His work is marked by a constant desire to learn and change.

Get in Touch
Your Ideas, Our Strategy – Let's Connect.

No commitment required. Whether you’re a charity, business, start-up or you just have an idea – we’re happy to talk through your project.

Embrace a worry-free experience as we proactively update, secure, and optimize your software, enabling you to focus on what matters most – driving innovation and achieving your business goals.

Hire Your A-Team Here to Unlock Potential & Drive Results
You can send an email to contact@wirefuture.com
clutch wirefuture profile designrush wirefuture profile goodfirms wirefuture profile good firms award-4 award-5 award-6