├── .dockerignore ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── WebApiWithBackgroundWorker.Common ├── Messaging │ ├── IBusConnection.cs │ ├── Message.cs │ └── RabbitPersistentConnection.cs └── WebApiWithBackgroundWorker.Common.csproj ├── WebApiWithBackgroundWorker.Publisher ├── Program.cs ├── RabbitPublisher.cs ├── WebApiWithBackgroundWorker.Publisher.csproj └── appsettings.json ├── WebApiWithBackgroundWorker.Subscriber ├── Controllers │ └── MessagesController.cs ├── Messaging │ ├── BackgroundSubscriberWorker.cs │ ├── Consumer.cs │ ├── IConsumer.cs │ ├── IMessagesRepository.cs │ ├── IProducer.cs │ ├── ISubscriber.cs │ ├── InMemoryMessagesRepository.cs │ ├── Producer.cs │ ├── RabbitSubscriber.cs │ └── RabbitSubscriberEventArgs.cs ├── Program.cs ├── Startup.cs ├── WebApiWithBackgroundWorker.Subscriber.csproj ├── WebApiWithBackgroundWorker.Subscriber.csproj.user └── appsettings.json ├── WebApiWithBackgroundWorker.sln ├── docker-compose.yml └── readme.md /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | v16/ 4 | .vs/ 5 | launchSettings.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/WebApiWithBackgroundWorker.Publisher/bin/Debug/netcoreapp3.1/WebApiWithBackgroundWorker.Publisher.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/WebApiWithBackgroundWorker.Publisher", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Launch (web)", 22 | "type": "coreclr", 23 | "request": "launch", 24 | "preLaunchTask": "build", 25 | // If you have changed target frameworks, make sure to update the program path. 26 | "program": "${workspaceFolder}/WebApiWithBackgroundWorker.Subscriber/bin/Debug/netcoreapp3.1/WebApiWithBackgroundWorker.Subscriber.dll", 27 | "args": [], 28 | "cwd": "${workspaceFolder}/WebApiWithBackgroundWorker.Subscriber", 29 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 30 | "console": "internalConsole", 31 | "stopAtEntry": false 32 | }, 33 | { 34 | "name": ".NET Core Attach", 35 | "type": "coreclr", 36 | "request": "attach", 37 | "processId": "${command:pickProcess}" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/WebApiWithBackgroundWorker.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/WebApiWithBackgroundWorker.Publisher/WebApiWithBackgroundWorker.Publisher.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/WebApiWithBackgroundWorker.Publisher/WebApiWithBackgroundWorker.Publisher.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Common/Messaging/IBusConnection.cs: -------------------------------------------------------------------------------- 1 | using RabbitMQ.Client; 2 | 3 | namespace WebApiWithBackgroundWorker.Common.Messaging 4 | { 5 | public interface IBusConnection 6 | { 7 | bool IsConnected { get; } 8 | 9 | IModel CreateChannel(); 10 | } 11 | } -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Common/Messaging/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApiWithBackgroundWorker.Common.Messaging 4 | { 5 | public class Message 6 | { 7 | public Guid Id { get; set; } 8 | public DateTime CreationDate { get; set; } 9 | public string Text { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Common/Messaging/RabbitPersistentConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RabbitMQ.Client; 3 | 4 | namespace WebApiWithBackgroundWorker.Common.Messaging 5 | { 6 | 7 | public class RabbitPersistentConnection : IDisposable, IBusConnection 8 | { 9 | private readonly IConnectionFactory _connectionFactory; 10 | private IConnection _connection; 11 | private bool _disposed; 12 | 13 | private readonly object semaphore = new object(); 14 | 15 | public RabbitPersistentConnection(IConnectionFactory connectionFactory) 16 | { 17 | _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); 18 | } 19 | 20 | public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed; 21 | 22 | public void Dispose() 23 | { 24 | if (_disposed) 25 | return; 26 | 27 | _disposed = true; 28 | 29 | _connection.Dispose(); 30 | } 31 | 32 | private void TryConnect() 33 | { 34 | lock (semaphore) 35 | { 36 | if (IsConnected) 37 | return; 38 | 39 | _connection = _connectionFactory.CreateConnection(); 40 | _connection.ConnectionShutdown += (s, e) => TryConnect(); 41 | _connection.CallbackException += (s, e) => TryConnect(); 42 | _connection.ConnectionBlocked += (s, e) => TryConnect(); 43 | } 44 | } 45 | 46 | public IModel CreateChannel() 47 | { 48 | TryConnect(); 49 | 50 | if (!IsConnected) 51 | throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); 52 | 53 | 54 | return _connection.CreateModel(); 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Common/WebApiWithBackgroundWorker.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Publisher/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WebApiWithBackgroundWorker.Common.Messaging; 3 | using RabbitMQ.Client; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace WebApiWithBackgroundWorker.Publisher 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | var builder = new ConfigurationBuilder(); 13 | builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); 14 | 15 | var config = builder.Build(); 16 | 17 | var rabbitConfig = config.GetSection("RabbitMQ"); 18 | var connectionFactory = new ConnectionFactory() 19 | { 20 | HostName = rabbitConfig["HostName"], 21 | UserName = rabbitConfig["UserName"], 22 | Password = rabbitConfig["Password"], 23 | VirtualHost = rabbitConfig["VirtualHost"], 24 | Port = AmqpTcpEndpoint.UseDefaultPort 25 | }; 26 | 27 | var connection = new RabbitPersistentConnection(connectionFactory); 28 | var publisher = new RabbitPublisher(connection, rabbitConfig["Exchange"]); 29 | 30 | while (true) 31 | { 32 | Console.ForegroundColor = ConsoleColor.Yellow; 33 | Console.WriteLine("type your messages and press ENTER to send. Press CTRL+C to quit."); 34 | 35 | var text = Console.ReadLine(); 36 | 37 | try 38 | { 39 | var message = new Message() 40 | { 41 | Id = Guid.NewGuid(), 42 | CreationDate = DateTime.UtcNow, 43 | Text = text 44 | }; 45 | publisher.Publish(message); 46 | 47 | Console.ForegroundColor = ConsoleColor.Green; 48 | Console.WriteLine("message sent!"); 49 | } 50 | catch (Exception ex) 51 | { 52 | Console.ForegroundColor = ConsoleColor.Red; 53 | Console.WriteLine($"an error has occurred while sending the message: {ex.Message}"); 54 | } 55 | 56 | Console.ResetColor(); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Publisher/RabbitPublisher.cs: -------------------------------------------------------------------------------- 1 | using RabbitMQ.Client; 2 | using System; 3 | using WebApiWithBackgroundWorker.Common.Messaging; 4 | using System.Text.Json; 5 | using System.Text; 6 | 7 | namespace WebApiWithBackgroundWorker.Publisher 8 | { 9 | public class RabbitPublisher : IDisposable 10 | { 11 | private readonly string _exchangeName; 12 | 13 | private readonly IBusConnection _connection; 14 | private IModel _channel; 15 | private readonly IBasicProperties _properties; 16 | 17 | public RabbitPublisher(IBusConnection connection, string exchangeName) 18 | { 19 | if (string.IsNullOrWhiteSpace(exchangeName)) 20 | throw new ArgumentException($"'{nameof(exchangeName)}' cannot be null or whitespace", nameof(exchangeName)); 21 | _exchangeName = exchangeName; 22 | 23 | _connection = connection ?? throw new ArgumentNullException(nameof(connection)); 24 | _channel = _connection.CreateChannel(); 25 | _channel.ExchangeDeclare(exchange: _exchangeName, type: ExchangeType.Fanout); 26 | _properties = _channel.CreateBasicProperties(); 27 | } 28 | 29 | public void Publish(Message message) 30 | { 31 | var jsonData = JsonSerializer.Serialize(message); 32 | 33 | var body = Encoding.UTF8.GetBytes(jsonData); 34 | 35 | _channel.BasicPublish( 36 | exchange: _exchangeName, 37 | routingKey: string.Empty, 38 | mandatory: true, 39 | basicProperties: _properties, 40 | body: body); 41 | } 42 | 43 | public void Dispose() 44 | { 45 | _channel?.Dispose(); 46 | _channel = null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Publisher/WebApiWithBackgroundWorker.Publisher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | 9dc39d6f-a7a9-4eed-80ef-bf4962b81ee3 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | Always 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Publisher/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RabbitMQ": { 3 | "Hostname": "127.0.0.1", 4 | "UserName": "guest", 5 | "Password": "guest", 6 | "VirtualHost": "/WebApiWithBackgroundWorker", 7 | "Exchange": "messages" 8 | } 9 | } -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using WebApiWithBackgroundWorker.Subscriber.Messaging; 4 | 5 | namespace WebApiWithBackgroundWorker.Controllers 6 | { 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class MessagesController : ControllerBase 10 | { 11 | private readonly IMessagesRepository _messagesRepository; 12 | 13 | public MessagesController(IMessagesRepository messagesRepository) 14 | { 15 | _messagesRepository = messagesRepository ?? throw new ArgumentNullException(nameof(messagesRepository)); 16 | } 17 | 18 | [HttpGet] 19 | public IActionResult Get() 20 | { 21 | var messages = _messagesRepository.GetMessages(); 22 | return Ok(messages); 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/BackgroundSubscriberWorker.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 10 | { 11 | public class BackgroundSubscriberWorker : BackgroundService 12 | { 13 | private readonly ISubscriber _subscriber; 14 | private readonly ILogger _logger; 15 | 16 | private readonly IProducer _producer; 17 | private readonly IEnumerable _consumers; 18 | 19 | public BackgroundSubscriberWorker(ISubscriber subscriber, IProducer producer, IEnumerable consumers, ILogger logger) 20 | { 21 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 22 | 23 | _subscriber = subscriber ?? throw new ArgumentNullException(nameof(subscriber)); 24 | _subscriber.OnMessage += OnMessageAsync; 25 | 26 | _producer = producer ?? throw new ArgumentNullException(nameof(producer)); 27 | _consumers = consumers ?? Enumerable.Empty(); 28 | } 29 | 30 | private async Task OnMessageAsync(object sender, RabbitSubscriberEventArgs args) 31 | { 32 | _logger.LogInformation($"got a new message: {args.Message.Text} at {args.Message.CreationDate}"); 33 | 34 | await _producer.PublishAsync(args.Message); 35 | } 36 | 37 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 38 | { 39 | _subscriber.Start(); 40 | 41 | var consumerTasks = _consumers.Select(c => c.BeginConsumeAsync(stoppingToken)); 42 | await Task.WhenAll(consumerTasks); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/Consumer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Threading.Channels; 3 | using System.Threading; 4 | using System; 5 | using WebApiWithBackgroundWorker.Common.Messaging; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 9 | { 10 | public class Consumer : IConsumer 11 | { 12 | private readonly ChannelReader _reader; 13 | private readonly ILogger _logger; 14 | 15 | private readonly IMessagesRepository _messagesRepository; 16 | private readonly int _instanceId; 17 | private static readonly Random Random = new Random(); 18 | 19 | public Consumer(ChannelReader reader, ILogger logger, int instanceId, IMessagesRepository messagesRepository) 20 | { 21 | _reader = reader; 22 | _instanceId = instanceId; 23 | _logger = logger; 24 | _messagesRepository = messagesRepository; 25 | } 26 | 27 | public async Task BeginConsumeAsync(CancellationToken cancellationToken = default) 28 | { 29 | _logger.LogInformation($"Consumer {_instanceId} > starting"); 30 | 31 | try 32 | { 33 | await foreach (var message in _reader.ReadAllAsync(cancellationToken)) 34 | { 35 | _logger.LogInformation($"CONSUMER ({_instanceId})> Received message {message.Id} : {message.Text}"); 36 | await Task.Delay(500, cancellationToken); 37 | _messagesRepository.Add(message); 38 | } 39 | } 40 | catch (OperationCanceledException ex) 41 | { 42 | _logger.LogWarning($"Consumer {_instanceId} > forced stop"); 43 | } 44 | 45 | _logger.LogInformation($"Consumer {_instanceId} > shutting down"); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/IConsumer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Threading; 3 | 4 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 5 | { 6 | public interface IConsumer 7 | { 8 | Task BeginConsumeAsync(CancellationToken cancellationToken = default); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/IMessagesRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using WebApiWithBackgroundWorker.Common.Messaging; 3 | 4 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 5 | { 6 | public interface IMessagesRepository 7 | { 8 | void Add(Message message); 9 | IReadOnlyCollection GetMessages(); 10 | } 11 | } -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/IProducer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using WebApiWithBackgroundWorker.Common.Messaging; 3 | using System.Threading; 4 | 5 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 6 | { 7 | public interface IProducer 8 | { 9 | Task PublishAsync(Message message, CancellationToken cancellationToken = default); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/ISubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RabbitMQ.Client.Events; 3 | using WebApiWithBackgroundWorker.Common.Messaging; 4 | 5 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 6 | { 7 | public interface ISubscriber 8 | { 9 | void Start(); 10 | event AsyncEventHandler OnMessage; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/InMemoryMessagesRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using WebApiWithBackgroundWorker.Common.Messaging; 5 | 6 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 7 | { 8 | public class InMemoryMessagesRepository : IMessagesRepository 9 | { 10 | private readonly Queue _messages; 11 | 12 | public InMemoryMessagesRepository() 13 | { 14 | _messages = new Queue(); 15 | } 16 | 17 | public void Add(Message message) 18 | { 19 | _messages.Enqueue(message ?? throw new ArgumentNullException(nameof(message))); 20 | } 21 | 22 | public IReadOnlyCollection GetMessages() 23 | { 24 | return _messages.ToArray(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/Producer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using WebApiWithBackgroundWorker.Common.Messaging; 3 | using System.Threading.Channels; 4 | using Microsoft.Extensions.Logging; 5 | using System.Threading; 6 | 7 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 8 | { 9 | public class Producer : IProducer 10 | { 11 | private readonly ChannelWriter _writer; 12 | private readonly ILogger _logger; 13 | 14 | public Producer(ChannelWriter writer, ILogger logger) 15 | { 16 | _writer = writer; 17 | _logger = logger; 18 | } 19 | 20 | public async Task PublishAsync(Message message, CancellationToken cancellationToken = default) 21 | { 22 | await _writer.WriteAsync(message, cancellationToken); 23 | _logger.LogInformation($"Producer > published message {message.Id} '{message.Text}'"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/RabbitSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using RabbitMQ.Client; 6 | using RabbitMQ.Client.Events; 7 | using WebApiWithBackgroundWorker.Common.Messaging; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 11 | { 12 | public record RabbitSubscriberOptions(string ExchangeName, string QueueName, string DeadLetterExchangeName, string DeadLetterQueue); 13 | 14 | public class RabbitSubscriber : ISubscriber, IDisposable 15 | { 16 | private readonly IBusConnection _connection; 17 | private IModel _channel; 18 | private readonly ILogger _logger; 19 | 20 | private readonly RabbitSubscriberOptions _options; 21 | 22 | public RabbitSubscriber(IBusConnection connection, RabbitSubscriberOptions options, ILogger logger) 23 | { 24 | _connection = connection ?? throw new ArgumentNullException(nameof(connection)); 25 | _options = options ?? throw new ArgumentNullException(nameof(options)); 26 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 27 | } 28 | 29 | private void InitChannel() 30 | { 31 | _channel?.Dispose(); 32 | 33 | _channel = _connection.CreateChannel(); 34 | 35 | _channel.ExchangeDeclare(exchange: _options.DeadLetterExchangeName, type: ExchangeType.Fanout); 36 | _channel.QueueDeclare(queue: _options.DeadLetterQueue, 37 | durable: true, 38 | exclusive: false, 39 | autoDelete: false, 40 | arguments: null); 41 | _channel.QueueBind(_options.DeadLetterQueue, _options.DeadLetterExchangeName, routingKey: string.Empty, arguments: null); 42 | 43 | _channel.ExchangeDeclare(exchange: _options.ExchangeName, type: ExchangeType.Fanout); 44 | 45 | _channel.QueueDeclare(queue: _options.QueueName, 46 | durable: false, 47 | exclusive: false, 48 | autoDelete: true, 49 | arguments: null); 50 | 51 | _channel.QueueBind(_options.QueueName, _options.ExchangeName, string.Empty, null); 52 | 53 | _channel.CallbackException += (sender, ea) => 54 | { 55 | InitChannel(); 56 | InitSubscription(); 57 | }; 58 | } 59 | 60 | private void InitSubscription() 61 | { 62 | var consumer = new AsyncEventingBasicConsumer(_channel); 63 | 64 | consumer.Received += OnMessageReceivedAsync; 65 | 66 | _channel.BasicConsume(queue: _options.QueueName, autoAck: false, consumer: consumer); 67 | } 68 | 69 | private async Task OnMessageReceivedAsync(object sender, BasicDeliverEventArgs eventArgs) 70 | { 71 | var consumer = sender as IBasicConsumer; 72 | var channel = consumer?.Model ?? _channel; 73 | 74 | Message message = null; 75 | try 76 | { 77 | var body = Encoding.UTF8.GetString(eventArgs.Body.Span); 78 | message = JsonSerializer.Deserialize(body); 79 | await this.OnMessage(this, new RabbitSubscriberEventArgs(message)); 80 | 81 | channel.BasicAck(eventArgs.DeliveryTag, multiple: false); 82 | } 83 | catch(Exception ex) 84 | { 85 | var errMsg = (message is null) ? $"an error has occurred while processing a message: {ex.Message}" 86 | : $"an error has occurred while processing message '{message.Id}': {ex.Message}"; 87 | _logger.LogError(ex, errMsg); 88 | 89 | if (eventArgs.Redelivered) 90 | channel.BasicReject(eventArgs.DeliveryTag, requeue: false); 91 | else 92 | channel.BasicNack(eventArgs.DeliveryTag, multiple: false, requeue: true); 93 | } 94 | } 95 | 96 | public event AsyncEventHandler OnMessage; 97 | 98 | public void Start() 99 | { 100 | InitChannel(); 101 | InitSubscription(); 102 | } 103 | 104 | public void Dispose() 105 | { 106 | _channel?.Dispose(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Messaging/RabbitSubscriberEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WebApiWithBackgroundWorker.Common.Messaging; 3 | 4 | namespace WebApiWithBackgroundWorker.Subscriber.Messaging 5 | { 6 | public class RabbitSubscriberEventArgs : EventArgs{ 7 | public RabbitSubscriberEventArgs(Message message){ 8 | this.Message = message; 9 | } 10 | 11 | public Message Message{get;} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace WebApiWithBackgroundWorker.Subscriber 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration(builder => 17 | { 18 | builder.AddUserSecrets(); 19 | }) 20 | .ConfigureWebHostDefaults(webBuilder => 21 | { 22 | webBuilder.UseStartup(); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.OpenApi.Models; 10 | using RabbitMQ.Client; 11 | using WebApiWithBackgroundWorker.Common.Messaging; 12 | using WebApiWithBackgroundWorker.Subscriber.Messaging; 13 | 14 | namespace WebApiWithBackgroundWorker.Subscriber 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 19 | { 20 | Configuration = configuration; 21 | Environment = env; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | public IWebHostEnvironment Environment { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddControllers(); 31 | services.AddSwaggerGen(c => 32 | { 33 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApiWithBackgroundWorker.Subscriber", Version = "v1" }); 34 | }); 35 | 36 | services.AddSingleton(); 37 | 38 | var rabbitConfig = Configuration.GetSection("RabbitMQ"); 39 | var exchangeName = rabbitConfig["Exchange"]; 40 | var queueName = rabbitConfig["Queue"]; 41 | var deadLetterExchange = rabbitConfig["DeadLetterExchange"]; 42 | var deadLetterQueue = rabbitConfig["DeadLetterQueue"]; 43 | var subscriberOptions = new RabbitSubscriberOptions(exchangeName, queueName, deadLetterExchange, deadLetterQueue); 44 | services.AddSingleton(subscriberOptions); 45 | 46 | var connectionFactory = new ConnectionFactory() 47 | { 48 | HostName = rabbitConfig["HostName"], 49 | UserName = rabbitConfig["UserName"], 50 | Password = rabbitConfig["Password"], 51 | VirtualHost = rabbitConfig["VirtualHost"], 52 | Port = AmqpTcpEndpoint.UseDefaultPort, 53 | DispatchConsumersAsync = true // this is mandatory to have Async Subscribers 54 | }; 55 | services.AddSingleton(connectionFactory); 56 | 57 | services.AddSingleton(); 58 | services.AddSingleton(); 59 | 60 | var channel = System.Threading.Channels.Channel.CreateBounded(100); 61 | services.AddSingleton(channel); 62 | 63 | services.AddSingleton(ctx => 64 | { 65 | var channel = ctx.GetRequiredService>(); 66 | var logger = ctx.GetRequiredService>(); 67 | return new Producer(channel.Writer, logger); 68 | }); 69 | 70 | services.AddSingleton>(ctx => 71 | { 72 | var channel = ctx.GetRequiredService>(); 73 | var logger = ctx.GetRequiredService>(); 74 | var repo = ctx.GetRequiredService(); 75 | 76 | var consumers = Enumerable.Range(1, 10) 77 | .Select(i => new Consumer(channel.Reader, logger, i, repo)) 78 | .ToArray(); 79 | return consumers; 80 | }); 81 | 82 | services.AddHostedService(); 83 | } 84 | 85 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 86 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 87 | { 88 | if (!env.IsProduction()) 89 | { 90 | app.UseDeveloperExceptionPage(); 91 | app.UseSwagger(); 92 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApiWithBackgroundWorker.Subscriber v1")); 93 | } 94 | 95 | app.UseHttpsRedirection(); 96 | 97 | app.UseRouting(); 98 | 99 | app.UseAuthorization(); 100 | 101 | app.UseEndpoints(endpoints => 102 | { 103 | endpoints.MapControllers(); 104 | }); 105 | 106 | app.UseWelcomePage(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/WebApiWithBackgroundWorker.Subscriber.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | c614e222-d239-4a80-82d8-f5b82c0fe5b3 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | Always 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/WebApiWithBackgroundWorker.Subscriber.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Docker 5 | 6 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.Subscriber/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RabbitMQ": { 3 | "Hostname": "127.0.0.1", 4 | "UserName": "guest", 5 | "Password": "guest", 6 | "Exchange": "messages", 7 | "Queue": "messages.workers", 8 | "DeadLetterExchange": "messages.dead", 9 | "DeadLetterQueue": "messages.dead.workers", 10 | "VirtualHost": "/WebApiWithBackgroundWorker" 11 | }, 12 | "Logging": { 13 | "LogLevel": { 14 | "Default": "Information", 15 | "Microsoft": "Warning", 16 | "Microsoft.Hosting.Lifetime": "Information" 17 | } 18 | }, 19 | "AllowedHosts": "*" 20 | } 21 | -------------------------------------------------------------------------------- /WebApiWithBackgroundWorker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33424.131 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiWithBackgroundWorker.Publisher", "WebApiWithBackgroundWorker.Publisher\WebApiWithBackgroundWorker.Publisher.csproj", "{6F8B53D5-D2A3-4F3F-83AB-D3DAF5E065EC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiWithBackgroundWorker.Subscriber", "WebApiWithBackgroundWorker.Subscriber\WebApiWithBackgroundWorker.Subscriber.csproj", "{62603142-FCCB-4A2D-920A-C2B7128C44C2}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiWithBackgroundWorker.Common", "WebApiWithBackgroundWorker.Common\WebApiWithBackgroundWorker.Common.csproj", "{AAE402C6-D876-464B-8BD5-5FD1B335E42A}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {6F8B53D5-D2A3-4F3F-83AB-D3DAF5E065EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {6F8B53D5-D2A3-4F3F-83AB-D3DAF5E065EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {6F8B53D5-D2A3-4F3F-83AB-D3DAF5E065EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {6F8B53D5-D2A3-4F3F-83AB-D3DAF5E065EC}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {62603142-FCCB-4A2D-920A-C2B7128C44C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {62603142-FCCB-4A2D-920A-C2B7128C44C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {62603142-FCCB-4A2D-920A-C2B7128C44C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {62603142-FCCB-4A2D-920A-C2B7128C44C2}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {AAE402C6-D876-464B-8BD5-5FD1B335E42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {AAE402C6-D876-464B-8BD5-5FD1B335E42A}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {AAE402C6-D876-464B-8BD5-5FD1B335E42A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {AAE402C6-D876-464B-8BD5-5FD1B335E42A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {CC65BD44-1062-439D-B22A-2F00DAAFB74C} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | webapiwithbackgroundworker.infrastructure.rabbitmq: 5 | image: rabbitmq:3-management-alpine 6 | container_name: webapiwithbackgroundworker.infrastructure.rabbitmq 7 | restart: always 8 | environment: 9 | RABBITMQ_DEFAULT_VHOST: "/WebApiWithBackgroundWorker" 10 | ports: 11 | - "15671:15671" 12 | - "15672:15672" 13 | - "5672:5672" 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Description 2 | This repository contains a simple implementation of Pub/Sub in .NET Core. This code has been used as example accompaining a series of articles on my personal blog: https://www.davidguida.net/consuming-message-queues-using-net-core-background-workers-part-1-message-queues/ 3 | 4 | 5 | ## The Publisher 6 | The Publisher is implemented a simple Console application. The user will be prompted to write a text message which will be sent to a RabbitMQ fanout exchange. 7 | 8 | ## The Subscriber 9 | The Subscriber is implemented as a .NET Core Background Worker hosted in a Web API. The Worker is starting the subscriber and listening for incoming messages. 10 | 11 | Messages are internally processed using a Producer/Consumer mechanism leveraging the System.Threading.Channels library. 12 | 13 | Once a message is received, the worker will send it to a Producer which will then dispatch on a Channel. A certain number of Consumers has been registered at bootstrap. The first available Consumer will pick up the message and store it in a repository. 14 | 15 | The Web API exposes a single GET endpoint /messages which will return the list of received messages. 16 | 17 | For more details about Producer/Consumer, check this article: https://www.davidguida.net/how-to-implement-producer-consumer-with-system-threading-channels/ 18 | --------------------------------------------------------------------------------