Dreamine.Communication.RabbitMQ 1.0.1
Dreamine.Communication.RabbitMQ 통신 기능과 관련 API를 제공합니다.
로딩중...
검색중...
일치하는것 없음
RabbitMqMessageBus.cs
이 파일의 문서화 페이지로 가기
1using System.Text.Json;
2using Dreamine.Communication.Abstractions.Enums;
3using Dreamine.Communication.Abstractions.Interfaces;
4using Dreamine.Communication.Abstractions.Models;
7
9
26public sealed class RabbitMqMessageBus : IMessageBus
27{
54 private readonly Dictionary<string, List<Func<MessageEnvelope, CancellationToken, Task>>> _handlers = new();
63 private readonly object _syncRoot = new();
64
91 private string? _consumerTag;
92
110 : this(options, new RabbitMqClientConnectionFactory())
111 {
112 }
113
148 IRabbitMqConnectionFactory connectionFactory)
149 {
150 _options = options ?? throw new ArgumentNullException(nameof(options));
151 _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
153 }
154
163 public ConnectionState State { get; private set; } = ConnectionState.Disconnected;
164
173 public TransportKind Kind => TransportKind.RabbitMq;
174
199 public Task ConnectAsync(CancellationToken cancellationToken = default)
200 {
201 cancellationToken.ThrowIfCancellationRequested();
202
203 if (State == ConnectionState.Connected)
204 {
205 return Task.CompletedTask;
206 }
207
208 State = ConnectionState.Connecting;
209
210 try
211 {
212 _connection = _connectionFactory.CreateConnection(_options);
213 _channel = _connection.CreateChannel();
214
216
217 State = ConnectionState.Connected;
218 return Task.CompletedTask;
219 }
220 catch
221 {
222 State = ConnectionState.Faulted;
223 Cleanup();
224 throw;
225 }
226 }
227
268 public Task PublishAsync(
269 MessageEnvelope message,
270 CancellationToken cancellationToken = default)
271 {
272 ArgumentNullException.ThrowIfNull(message);
273 cancellationToken.ThrowIfCancellationRequested();
274
275 if (_channel is null || State != ConnectionState.Connected)
276 {
277 throw new InvalidOperationException("RabbitMQ message bus is not connected.");
278 }
279
280 var route = string.IsNullOrWhiteSpace(message.Route)
281 ? _options.RoutingKey
282 : message.Route;
283
284 var body = JsonSerializer.SerializeToUtf8Bytes(message);
285
286 var properties = _channel.CreateBasicProperties();
287 properties.Persistent = _options.PersistentMessages;
288 properties.ContentType = "application/json";
289 properties.Type = nameof(MessageEnvelope);
290
291 _channel.BasicPublish(
292 exchange: _options.ExchangeName,
293 routingKey: route,
294 mandatory: false,
295 properties: properties,
296 body: body);
297
298 return Task.CompletedTask;
299 }
300
349 public Task SubscribeAsync(
350 string route,
351 Func<MessageEnvelope, CancellationToken, Task> handler,
352 CancellationToken cancellationToken = default)
353 {
354 ArgumentException.ThrowIfNullOrWhiteSpace(route);
355 ArgumentNullException.ThrowIfNull(handler);
356 cancellationToken.ThrowIfCancellationRequested();
357
358 if (_channel is null || State != ConnectionState.Connected)
359 {
360 throw new InvalidOperationException("RabbitMQ message bus is not connected.");
361 }
362
363 lock (_syncRoot)
364 {
365 if (!_handlers.TryGetValue(route, out var handlers))
366 {
367 handlers = [];
368 _handlers[route] = handlers;
369 }
370
371 handlers.Add(handler);
372 }
373
374 _channel.QueueBind(
375 queue: _options.QueueName,
376 exchange: _options.ExchangeName,
377 routingKey: route);
378
379 if (!string.IsNullOrWhiteSpace(_consumerTag))
380 {
381 return Task.CompletedTask;
382 }
383
384 _consumerTag = _channel.BasicConsume(
385 queue: _options.QueueName,
386 autoAck: false,
387 onReceived: (delivery, token) => HandleReceivedAsync(
388 delivery,
389 token.CanBeCanceled ? token : cancellationToken));
390
391 return Task.CompletedTask;
392 }
393
418 public Task DisconnectAsync(CancellationToken cancellationToken = default)
419 {
420 cancellationToken.ThrowIfCancellationRequested();
421
422 State = ConnectionState.Disconnecting;
423
424 try
425 {
426 Cleanup();
427 State = ConnectionState.Disconnected;
428 return Task.CompletedTask;
429 }
430 catch
431 {
432 State = ConnectionState.Faulted;
433 throw;
434 }
435 }
436
453 public async ValueTask DisposeAsync()
454 {
455 await DisconnectAsync().ConfigureAwait(false);
456 }
457
498 private async Task HandleReceivedAsync(
499 RabbitMqDelivery delivery,
500 CancellationToken cancellationToken)
501 {
502 if (_channel is null)
503 {
504 return;
505 }
506
507 try
508 {
509 var message = JsonSerializer.Deserialize<MessageEnvelope>(delivery.Body.Span);
510
511 if (message is null)
512 {
513 _channel.BasicReject(delivery.DeliveryTag, requeue: false);
514 return;
515 }
516
517 var route = string.IsNullOrWhiteSpace(message.Route)
518 ? delivery.RoutingKey
519 : message.Route;
520
521 List<Func<MessageEnvelope, CancellationToken, Task>> handlers;
522
523 lock (_syncRoot)
524 {
525 if (!_handlers.TryGetValue(route, out var routeHandlers) &&
526 !_handlers.TryGetValue(delivery.RoutingKey, out routeHandlers))
527 {
528 handlers = [];
529 }
530 else
531 {
532 handlers = new List<Func<MessageEnvelope, CancellationToken, Task>>(routeHandlers);
533 }
534 }
535
536 if (handlers.Count == 0)
537 {
538 _channel.BasicAck(delivery.DeliveryTag, multiple: false);
539 return;
540 }
541
542 foreach (var handler in handlers)
543 {
544 await handler(message, cancellationToken).ConfigureAwait(false);
545 }
546
547 _channel.BasicAck(delivery.DeliveryTag, multiple: false);
548 }
549 catch
550 {
551 try
552 {
553 _channel.BasicNack(
554 delivery.DeliveryTag,
555 multiple: false,
556 requeue: _options.RequeueOnHandlerError);
557 }
558 catch
559 {
560 // Ignore acknowledgement failure during shutdown or channel fault.
561 }
562
563 throw;
564 }
565 }
566
583 private void DeclareTopology(IRabbitMqChannel channel)
584 {
585 channel.ExchangeDeclare(
586 exchange: _options.ExchangeName,
587 type: _options.ExchangeType,
588 durable: _options.Durable,
589 autoDelete: _options.AutoDelete);
590
591 channel.QueueDeclare(
592 queue: _options.QueueName,
593 durable: _options.Durable,
594 exclusive: _options.Exclusive,
595 autoDelete: _options.AutoDelete);
596
597 channel.QueueBind(
598 queue: _options.QueueName,
599 exchange: _options.ExchangeName,
600 routingKey: _options.RoutingKey);
601 }
602
611 private void Cleanup()
612 {
613 lock (_syncRoot)
614 {
615 _handlers.Clear();
616 }
617
618 if (_channel is not null)
619 {
620 try
621 {
622 if (!string.IsNullOrWhiteSpace(_consumerTag))
623 {
624 _channel.BasicCancel(_consumerTag);
625 }
626 }
627 catch
628 {
629 // Ignore consumer cancel failure during cleanup.
630 }
631
632 try
633 {
634 if (_channel.IsOpen)
635 {
636 _channel.Close();
637 }
638 }
639 catch
640 {
641 // Ignore channel close failure during cleanup.
642 }
643
644 _channel.Dispose();
645 _channel = null;
646 }
647
648 if (_connection is not null)
649 {
650 try
651 {
652 if (_connection.IsOpen)
653 {
654 _connection.Close();
655 }
656 }
657 catch
658 {
659 // Ignore connection close failure during cleanup.
660 }
661
662 _connection.Dispose();
663 _connection = null;
664 }
665
666 _consumerTag = null;
667 }
668
701 private static void ValidateOptions(RabbitMqMessageBusOptions options)
702 {
703 ArgumentException.ThrowIfNullOrWhiteSpace(options.HostName);
704 ArgumentException.ThrowIfNullOrWhiteSpace(options.VirtualHost);
705 ArgumentException.ThrowIfNullOrWhiteSpace(options.UserName);
706 ArgumentException.ThrowIfNullOrWhiteSpace(options.Password);
707 ArgumentException.ThrowIfNullOrWhiteSpace(options.ExchangeName);
708 ArgumentException.ThrowIfNullOrWhiteSpace(options.QueueName);
709 ArgumentException.ThrowIfNullOrWhiteSpace(options.RoutingKey);
710 ArgumentException.ThrowIfNullOrWhiteSpace(options.ExchangeType);
711
712 if (options.Port <= 0 || options.Port > 65535)
713 {
714 throw new ArgumentOutOfRangeException(nameof(options.Port));
715 }
716 }
717}
static void ValidateOptions(RabbitMqMessageBusOptions options)
async Task HandleReceivedAsync(RabbitMqDelivery delivery, CancellationToken cancellationToken)
Task PublishAsync(MessageEnvelope message, CancellationToken cancellationToken=default)
Task DisconnectAsync(CancellationToken cancellationToken=default)
readonly Dictionary< string, List< Func< MessageEnvelope, CancellationToken, Task > > > _handlers
RabbitMqMessageBus(RabbitMqMessageBusOptions options, IRabbitMqConnectionFactory connectionFactory)
Task ConnectAsync(CancellationToken cancellationToken=default)
Task SubscribeAsync(string route, Func< MessageEnvelope, CancellationToken, Task > handler, CancellationToken cancellationToken=default)
void QueueBind(string queue, string exchange, string routingKey)
void QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete)
void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete)