├── .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 |
--------------------------------------------------------------------------------