Поддерживаемые платформы:
.NET Core 3.1+
Ознакомьтесь с последними изменениями в журнале изменений.
MyLab.Mq
- библиотека, содержащая инструменты для работы с очередью сообщений в реализации RabbitMQ
. Разработана на базе официального клиента RabbitMQ.NET.
Аспекты организации отправки сообщений:
- объявлении объектной модели сообщения;
- привязка объектной модели сообщения к объекту очереди: очередь/обменник;
- загрузка конфигурации;
- регистрация реализации отправителя сообщений в DI-контейнере;
- получение отправителя сообщений в качестве зависимости;
- отправка объектов сообщения.
Аспекты организации получения сообщений:
- объявлении объектной модели сообщения;
- привязка объектной модели сообщения к объекту очереди: очередь/обменник;
- загрузка конфигурации;
- регистрация потребителей сообщений;
- обработка полученных сообщений в потребителях.
В качестве содержательной части сообщения можно передавать любое значение или объект.
Модель сообщения, описанная в виде класса может быть помечена атрибутом MqAttribute
, который позволяет указать для модели некоторые параметры по умолчанию:
- имя обменника;
- имя роутинга.
Ниже представлены примеры использования атрибута MqAttribute
:
- по умолчанию сообщение будет отправляться в очередь
my:test-queue
[Mq(Routing = "my:test-queue")]
class MsgPayload
{
public string Value { get; set; }
}
- по умолчанию сообщение будет отправляться в обменник
my:test-exch
с пустым ключом роутинга
[Mq(Exchange = "my:test-exch")]
class MsgPayload
{
public string Value { get; set; }
}
- по умолчанию сообщение будет отправляться в обменник
my:test-exch
с ключом роутингаfoo
[Mq(Exchange = "my:test-exch", Routing = "foo")]
class MsgPayload
{
public string Value { get; set; }
}
В инфраструктуре сообщение представляется в виде класса MqMessage<TPayload>
, где TPayload
- тип модели бизнес-сообщения. Он используется как для отправки, так и для получения сообщений. Содержит обычные реквизиты для сообщения в контексте MQ.
Объектная модель для ознакомления:
/// <summary>
/// Contains MQ message data
/// </summary>
/// <typeparam name="T">payload type</typeparam>
public class MqMessage<T>
{
/// <summary>
/// Message identifier. <see cref="Guid.NewGuid"/> by default
/// </summary>
public Guid MessageId { get; set; }
/// <summary>
/// Message correlated to this one
/// </summary>
public Guid CorrelationId { get; set; }
/// <summary>
/// Gets response publish parameters
/// </summary>
public string ReplyTo { get; set; }
/// <summary>
/// Headers
/// </summary>
public MqHeader[] Headers { get; set; }
/// <summary>
/// Message payload
/// </summary>
public T Payload { get; }
/// <summary>
/// Initializes a new instance of <see cref="MqMessage{T}"/>
/// </summary>
public MqMessage(T payload)
{
//....
}
}
/// <summary>
/// Represent MQ message header
/// </summary>
public class MqHeader
{
/// <summary>
/// Header name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Header value
/// </summary>
public string Value { get; set; }
}
Для публикации сообщения необходимо:
- загрузить конфигурацию подключения (Конфигурирование)
- добавить сервис публикации сообщений
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMqPublisher();
...
}
- объявить объектную модель сообщения (Модель бизнес-сообщения)
- сервис отправления, как зависимость в классе-потребителе
- опубликовать сообщение.
Пример сервиса, отправляющего сообщение с помощью сервиса публикации сообщений:
public class SomeService
{
IMqPublisher _mqPublisher
public SomeService(IMqPublisher mqPublisher)
{
_mqPublisher = mqPublisher;
}
public void DoSomething()
{
....
_mqPublisher.Publish(new MsgPayload { Value = "foo-val" });
...
}
}
В примеры выше представлена весьма развёрнутая и наполненная дополнительными опциями публикация. Ниже приведён список некоторых вариантов публикации сообщений:
- публикация объекта сообщения в очередь/обменник по умолчанию
//Full
publisher.Publish(new OutgoingMqEnvelop<Msg>
{
Message = new MqMessage<Msg>(
new MsgPayload
{
Value = "foo-val"
})
});
//Short extension method
publisher.Publish(new MsgPayload { Value = "foo-val" });
- публикация с указанием целевой очереди:
//Full
publisher.Publish(new OutgoingMqEnvelop<Msg>
{
PublishTarget = new PublishTarget{ Routing = "my:another-queue" },
Message = new MqMessage<Msg>(
new MsgPayload
{
Value = "foo-val"
})
});
//Short extension method
publisher.PublishToQueue(new MsgPayload { Value = "foo-val" }, "my:another-queue");
- публикация с указанием целевого обменника:
//Full
publisher.Publish(new OutgoingMqEnvelop<Msg>
{
PublishTarget = new PublishTarget { Exchange = "my:another-exch" },
Message = new MqMessage<Msg>(
new MsgPayload
{
Value = "foo-val"
})
});
//Short extension method
publisher.PublishToExchange(new MsgPayload { Value = "foo-val" }, "my:another-exch");
- публикация с указанием целевого обменника и ключа роутинга:
//Full
publisher.Publish(new OutgoingMqEnvelop<Msg>
{
PublishTarget = new PublishTarget
{
Exchange = "my:another-exch" ,
Routing = "foo"
},
Message = new MqMessage<Msg>(
new MsgPayload
{
Value = "foo-val"
})
});
//Short extension method
publisher.PublishToExchange(
new MsgPayload { Value = "foo-val" },
"my:another-exch",
"foo");
- публикация с указанием остальных параметров отправляемого сообщения:
publisher.Publish(new OutgoingMqEnvelop<Msg>
{
PublishTarget = new PublishTarget
{
Routing = "my:test-queue"
},
Message = new MqMessage<Msg>(
new MsgPayload
{
Value = "foo-val"
})
{
ReplyTo = "foo-queue",
CorrelationId = correlationId,
MessageId = messageId,
Headers = new[]
{
new MqHeader {Name = "FooHeader", Value = "FooValue"},
}
}
});
Для потребления сообщения необходимо:
-
загрузить конфигурацию подключения (Конфигурирование);
-
реализовать логику потребления сообщений
-
зарегистрировать потребителей.
Логика потребление - реализация обработки полученных сообщений. Реализация логики - класс, реализующий интерфейс IMqConsumerLogic<TPayload>
для обработки сообщений по одному, и IMqBatchConsumerLogic<TPayload>
для реализации логики обработки нескольких сообщений одновременно.
Вот эти интерфейсы, для ознакомления:
/// <summary>
/// Represent messages queue consumer
/// </summary>
public interface IMqConsumerLogic<TMsgPayload>
{
Task Consume(MqMessage<TMsgPayload> message);
}
/// <summary>
/// Represent batch messages queue consumer
/// </summary>
public interface IMqBatchConsumerLogic<TMsgPayload>
{
Task Consume(IEnumerable<MqMessage<TMsgPayload>> messages);
}
Потребитель - один из наследников класса MqConsumer
. В общем случае не требуется разрабатывать наследника для решения регулярных задач. Для этого есть готовые реализации:
MqConsumer<TMsgPayload, TLogic>
- определяет обычного потребителя сообщений;MqBatchConsumer<TMsgPayload, TLogic>
- определяет потребителя сообщений, получающего по несколько сообщений сразу.
Здесь:
TMsgPayload
- тип бизнес-сообщения;TLogic
- тип логики потребления.
Регистрация потребителей осуществляется с помощью метода расширения для IServiceCollection
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMqConsuming(registrar =>
{
registrar.RegisterConsumer(
new MqConsumer<MsgPayload,MyConsumerLogic>(
"my:test-queue");
})
...
}
class MsgPayload
{
public string Value { get; set; }
}
class MyConsumerLogic : IMqConsumerLogic<MsgPayload>
{
Task Consume(MqMessage<MsgPayload> message)
{
// do something
}
}
В случае, если наименование или дополнительные опции создания потребителя содержатся в конфигурации, регистрация потребителя может выглядеть следующим образом:
services.AddMqConsuming(r =>
r.RegisterConsumerByOptions<MyOptions>(
opt => new MqConsumer<MsgPayload,MyConsumerLogic>(opt.Queue)
)
Для случая, когда регистрировать потребителя следует только при наличии в опциях определённого параметра:
services.AddMqConsuming(r =>
r.RegisterConsumerByOptions<MyOptions, string>(
opt => opt.Queue
queue => new MqConsumer<MsgPayload,MyConsumerLogic>(queue)
)
Если функция потребления должна быть активна только в случае, если указана соответствующая конфигурация, то это можно указать, используя параметр optional
при добавлении механизмов потребления в сервисы приложения:
services.AddMqConsuming(r => ..., options: true);
Значение параметра по умолчанию - false
.
Ниже приведены логи в случае, если не указаны параметры подключения к очереди:
- если потребитель подключен не опционально:
[2021-08-04 07:06:10Z] fail: MyLab.Mq.PubSub.MqConsumerHost[0]
Message: None of the specified endpoints were reachable
Time: 2021-08-04T10:06:10.313
Labels:
log_level: error
Exception:
Message: None of the specified endpoints were reachable
Type: RabbitMQ.Client.Exceptions.BrokerUnreachableException
....
-
если потребитель подключен опционально:
[2021-08-04 07:25:16Z] warn: MyLab.Mq.PubSub.MqConsumerHost[0] Message: Enabled indicator service indicate `false`. Consuming is not started. Time: 2021-08-04T10:25:16.877 Labels: log_level: warning
Конфигурирование позволяет загрузить параметры подключения к MQ серверу и автоматически их применять для публикации и потребления сообщений.
Объектная модель опций подключения выглядят следующим образом:
/// <summary>
/// Contains MQ connection options
/// </summary>
public class MqOptions
{
/// <summary>
/// Server host
/// </summary>
public string Host { get; set; }
/// <summary>
/// Virtual host
/// </summary>
public string VHost { get; set; }
/// <summary>
/// Port
/// </summary>
public int Port { get; set; } = 5672;
/// <summary>
/// Login user
/// </summary>
public string User { get; set; }
/// <summary>
/// Login password
/// </summary>
public string Password { get; set; }
}
В приложении конфигурирование осуществляется двумя методами расширения IServiceCollection
:
LoadMqConfig
- загружает настройки из конфигурации с возможностью указания имени узла (Mq - по умолчанию);ConfigureMq
- определяет настройки через делегат.
public void ConfigureServices(IServiceCollection services)
{
...
services.LoadMqConfig(Configuration)
...
services.ConfigureMq(o =>
{
o.Host = "myhost.com";
o.VHost = "test-host";
o.User = "foo";
o.Password = "foo-pass";
});
...
}
Пример конфигурационного файла с портом по умолчанию:
{
"Mq": {
"Host" : "myhost.com",
"VHost" : "test-host",
"User" : "foo",
"Password" : "foo-pass"
}
}
Для функционального тестирования приложения, осуществляющего обработку сообщений из очередей на базе MyLab.Mq
, рекомендуется использовать эмулятор входящих сообщений
(сервис с интерфейсом IInputMessageEmulator
).
/// <summary>
/// Specifies emulator of queue with input messages
/// </summary>
public interface IInputMessageEmulator
{
/// <summary>
/// Emulates queueing of message
/// </summary>
public Task<FakeMessageQueueProcResult> Queue(object message, string queue, IBasicProperties messageProps = null);
}
/// <summary>
/// Contains fake queue message processing result
/// </summary>
public class FakeMessageQueueProcResult
{
/// <summary>
/// Is there was acknowledge
/// </summary>
public bool Acked { get; set; }
/// <summary>
/// Is there was rejected
/// </summary>
public bool Rejected { get; set; }
/// <summary>
/// Exception which is reason of rejection
/// </summary>
public Exception RejectionException { get; set; }
/// <summary>
/// Requeue flag value
/// </summary>
public bool RequeueFlag { get; set; }
}
Для этого необходимо:
-
при конфигурировании приложения зарегистрировать эмулятор в сервисах:
services .AddMqConsuming(cr => cr.RegisterConsumer(consumer)) .AddMqMsgEmulator(); // <----
При этом не будет осуществляться подключение к реальной очереди для прослушивания очередей.
-
получить эмулятор
IInputMessageEmulator
из поставщика сервисов:var services = new ServiceCollection(); ... services .AddMqConsuming(cr => cr.RegisterConsumer(consumer)) .AddMqMsgEmulator(); var srvProvider = services.BuildServiceProvider(); var emulator = srvProvider.GetService<IInputMessageEmulator>(); // <----
Или в конструкторе объекта, создаваемого с использованием
DI
-
отправить тестовое сообщение:
await emulator.Queue(testMsg, "foo-queue");
При отправке через эмулятор, сообщение обрабатывается синхронно. Результатом обработки сообщения является объект типа FakeMessageQueueProcResult
(представлен выше). Он и является предметом анализа в тесте.
Важно! При использовании эмулятора, отключается механизм подключения к реальной очереди.
При тестировании рекомендуется рассмотреть использование единого экземпляра логики потребителя. Т.е. при регистрации создать объект логики потребителя и передать в потребитель, вместо подхода когда потребитель сам создаёт логику. При этом появляется возможность самостоятельно инициализировать объект логики, кастомизировав его, например, тестовым поведением.
var services = new ServiceCollection();
var logic = new TestConsumerLogic();
var consumer = new MqConsumer<TestEntity, TestConsumerLogic>("foo-queue", logic); // <----
var emulatorRegistrar = new InputMessageEmulatorRegistrar();
services.AddMqConsuming(
consumerRegistrar => consumerRegistrar.RegisterConsumer(consumer),
emulatorRegistrar
);
Объект логики и/или его тестовые зависимости являются предметом анализа в тесте.
При тестировании с реальным сервисом RabbitMQ
в тестах может потребоваться выполнить некоторые действия без интеграции в .NET Core
приложение и без связи с какими-то зависимостями.
Класс MqQueueFactory
- фабрика очередей. Создаёт очередь с указанными характеристиками.
Для целей тестирования, рекомендуется инициализировать фабрику, указывая префикс имён очередей и флаг AutoDelete
:
var queueFactory = new MqQueueFactory(connProvider)
{
Prefix = "prefix:",
AutoDelete = true
};
У фабрики есть несколько способов назначения имён создаваемым очередям:
-
указать точное имя
MqQueue queue = queueFactory.CreateWithName("foo"); //name: 'foo' //ignore prefix!!
-
указать идентификатор
MqQueue queue = queueFactory.CreateWithId("foo"); //name: 'prefix:foo'
-
назначить случайный идентификатор
MqQueue queue = queueFactory.CreateWithRandomId(); //name: 'prefix:4a2943bdfdc5434fa134c2c018635fea'
Класс MqExchangeFactory
- фабрика обменников. Создаёт обменник с указанными характеристиками.
Для целей тестирования, рекомендуется инициализировать фабрику, указывая префикс имён обменников и флаг AutoDelete
:
var exchangeFactory = new MqExchangeFactory(MqExchangeType.Fanout, connProvider)
{
Prefix = "prefix:",
AutoDelete = true
};
У фабрики есть несколько способов назначения имён создаваемым обменникам:
-
указать точное имя
MqExchange exchange = exchangeFactory.CreateWithName("foo"); //name: 'foo' //ignore prefix!!
-
указать идентификатор
MqExchange exchange = exchangeFactory.CreateWithId("foo"); //name: 'prefix:foo'
-
назначить случайный идентификатор
MqExchange exchange = exchangeFactory.CreateWithRandomId(); //name: 'prefix:4a2943bdfdc5434fa134c2c018635fea'
В примере ниже показано как осуществляется привязка очереди к обменнику:
MqQueue queue = ...
MqExchange exchange = ...
queue.BindToExchange(exchange, "foo-routing");
Публикация сообщения в очередь типа MqQueue
осуществляется через метод Publish
, в который можно передать произвольный объект, который будет сериализован в JSON и передан в очередь.
class Model
{
public string Value { get;set; } = 10
}
//...
queue.Publish(new Model());
//MQ Message content: {"Value":10}
Класс MqQueue
предоставляет возможность синхронного чтения одного сообщения из очереди:
MqMessageRead<TModel> next = queue.Listen<TModel>();
Есть возможность указать таймаут ожидания:
MqMessageRead<TModel> next = queue.Listen<TModel>(TimeSpan.FromSeconds(2));
Таймаут по умолчанию - 1 сек. В случае истечения заданного времени таймаута, возникнет исключение типа TimeoutException
.
Полученное сообщение следует подтвердить или отклонить: методы Ack
и Nack
, соответственно.
/// <summary>
/// Read message
/// </summary>
/// <typeparam name="T">payload type</typeparam>
public class MqMessageRead<T>
{
/// <summary>
/// Message
/// </summary>
public MqMessage<T> Message { get; }
/// <summary>
/// Initializes a new instance of <see cref="MqMessageRead{T}"/>
/// </summary>
public MqMessageRead(IModel model, ulong deliveryTag, MqMessage<T> message)
/// <summary>
/// Ack message
/// </summary>
public void Ack()
/// <summary>
/// Nack message
/// </summary>
public void Nack()
}
Также есть возможность читать с автоподтверждением:
MqMessage<T> next = queue.ListenAutoAck<TModel>()