-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add native RabbitMQ transport for outbox processor #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
4863537
feat: Add native RabbitMQ transport for outbox processor
Copilot dc117c3
refactor: Use ITopicNameResolver for routing key resolution
Copilot 315c84d
fix: Address PR review feedback
Copilot fd1ed22
fix: test failures in unit and integration tests
Copilot 5ff6138
test: Improve test to verify transport replacement correctly
Copilot 08b2afa
test: Add comprehensive unit tests for RabbitMQ transport
Copilot 7c8b9cc
refactor: RabbitMQ connection/channel open checks
samtrion 46227a3
refactor: Remove IConnection registration from RabbitMQ transport
Copilot a7aa811
docs: Document RabbitMqMessageTransport testing strategy
Copilot ebb01af
refactor: Add adapter pattern for RabbitMQ transport testability
Copilot 8ccaae9
fix: Suggested auto fix
samtrion File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/NetEvolve.Pulse.RabbitMQ/Internals/IRabbitMqChannelAdapter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| namespace NetEvolve.Pulse.Internals; | ||
|
|
||
| using RabbitMQ.Client; | ||
|
|
||
| /// <summary> | ||
| /// Adapter interface for RabbitMQ channel operations. | ||
| /// </summary> | ||
| internal interface IRabbitMqChannelAdapter : IDisposable | ||
| { | ||
| /// <summary> | ||
| /// Gets a value indicating whether the channel is open. | ||
| /// </summary> | ||
| bool IsOpen { get; } | ||
|
|
||
| /// <summary> | ||
| /// Publishes a message asynchronously. | ||
| /// </summary> | ||
| /// <typeparam name="TProperties">The type of basic properties.</typeparam> | ||
| /// <param name="exchange">The exchange to publish to.</param> | ||
| /// <param name="routingKey">The routing key.</param> | ||
| /// <param name="mandatory">Whether the message is mandatory.</param> | ||
| /// <param name="basicProperties">The message properties.</param> | ||
| /// <param name="body">The message body.</param> | ||
| /// <param name="cancellationToken">A token to monitor for cancellation requests.</param> | ||
| /// <returns>A task representing the asynchronous operation.</returns> | ||
| ValueTask BasicPublishAsync<TProperties>( | ||
| string exchange, | ||
| string routingKey, | ||
| bool mandatory, | ||
| TProperties basicProperties, | ||
| ReadOnlyMemory<byte> body, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| where TProperties : IReadOnlyBasicProperties, IAmqpHeader; | ||
| } |
19 changes: 19 additions & 0 deletions
19
src/NetEvolve.Pulse.RabbitMQ/Internals/IRabbitMqConnectionAdapter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| namespace NetEvolve.Pulse.Internals; | ||
|
|
||
| /// <summary> | ||
| /// Adapter interface for RabbitMQ connection operations. | ||
| /// </summary> | ||
| internal interface IRabbitMqConnectionAdapter | ||
| { | ||
| /// <summary> | ||
| /// Gets a value indicating whether the connection is open. | ||
| /// </summary> | ||
| bool IsOpen { get; } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new channel asynchronously. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">A token to monitor for cancellation requests.</param> | ||
| /// <returns>A task representing the asynchronous operation, containing the created channel adapter.</returns> | ||
| Task<IRabbitMqChannelAdapter> CreateChannelAsync(CancellationToken cancellationToken = default); | ||
| } |
39 changes: 39 additions & 0 deletions
39
src/NetEvolve.Pulse.RabbitMQ/Internals/RabbitMqChannelAdapter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| namespace NetEvolve.Pulse.Internals; | ||
|
|
||
| using RabbitMQ.Client; | ||
|
|
||
| /// <summary> | ||
| /// Adapter implementation that wraps RabbitMQ.Client IChannel. | ||
| /// </summary> | ||
| internal sealed class RabbitMqChannelAdapter : IRabbitMqChannelAdapter | ||
| { | ||
| private readonly IChannel _channel; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="RabbitMqChannelAdapter"/> class. | ||
| /// </summary> | ||
| /// <param name="channel">The underlying RabbitMQ channel.</param> | ||
| public RabbitMqChannelAdapter(IChannel channel) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(channel); | ||
| _channel = channel; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool IsOpen => _channel.IsOpen; | ||
|
|
||
| /// <inheritdoc /> | ||
| public ValueTask BasicPublishAsync<TProperties>( | ||
| string exchange, | ||
| string routingKey, | ||
| bool mandatory, | ||
| TProperties basicProperties, | ||
| ReadOnlyMemory<byte> body, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| where TProperties : IReadOnlyBasicProperties, IAmqpHeader => | ||
| _channel.BasicPublishAsync(exchange, routingKey, mandatory, basicProperties, body, cancellationToken); | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Dispose() => _channel.Dispose(); | ||
| } |
31 changes: 31 additions & 0 deletions
31
src/NetEvolve.Pulse.RabbitMQ/Internals/RabbitMqConnectionAdapter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| namespace NetEvolve.Pulse.Internals; | ||
|
|
||
| using RabbitMQ.Client; | ||
|
|
||
| /// <summary> | ||
| /// Adapter implementation that wraps RabbitMQ.Client IConnection. | ||
| /// </summary> | ||
| internal sealed class RabbitMqConnectionAdapter : IRabbitMqConnectionAdapter | ||
| { | ||
| private readonly IConnection _connection; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="RabbitMqConnectionAdapter"/> class. | ||
| /// </summary> | ||
| /// <param name="connection">The underlying RabbitMQ connection.</param> | ||
| public RabbitMqConnectionAdapter(IConnection connection) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(connection); | ||
| _connection = connection; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool IsOpen => _connection.IsOpen; | ||
|
|
||
| /// <inheritdoc /> | ||
| public async Task<IRabbitMqChannelAdapter> CreateChannelAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| var channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | ||
| return new RabbitMqChannelAdapter(channel); | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
src/NetEvolve.Pulse.RabbitMQ/NetEvolve.Pulse.RabbitMQ.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFrameworks>$(_ProjectTargetFrameworks)</TargetFrameworks> | ||
| <Description>RabbitMQ transport for the Pulse CQRS mediator outbox. Delivers outbox messages directly to RabbitMQ exchanges using the official .NET client, supporting single and batched sends with health checks for the broker connection.</Description> | ||
| <PackageTags>$(PackageTags);rabbitmq;amqp;</PackageTags> | ||
| <RootNamespace>NetEvolve.Pulse</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Options" /> | ||
| <PackageReference Include="RabbitMQ.Client" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\NetEvolve.Pulse.Extensibility\NetEvolve.Pulse.Extensibility.csproj" /> | ||
| </ItemGroup> | ||
| </Project> |
168 changes: 168 additions & 0 deletions
168
src/NetEvolve.Pulse.RabbitMQ/Outbox/RabbitMqMessageTransport.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| namespace NetEvolve.Pulse.Outbox; | ||
|
|
||
| using System.Text; | ||
| using Microsoft.Extensions.Options; | ||
| using NetEvolve.Pulse.Extensibility.Outbox; | ||
| using NetEvolve.Pulse.Internals; | ||
| using RabbitMQ.Client; | ||
|
|
||
| /// <summary> | ||
| /// Message transport that publishes outbox messages to RabbitMQ exchanges. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para><strong>Connection Management:</strong></para> | ||
| /// This transport uses an injected connection adapter and creates channels on demand. | ||
| /// The connection lifetime is managed externally via dependency injection. | ||
| /// <para><strong>Routing Key Resolution:</strong></para> | ||
| /// Each message is published with a routing key resolved by <see cref="ITopicNameResolver"/>. | ||
| /// By default, the simple class name of the event type is used (e.g., <c>"OrderCreated"</c>). | ||
| /// <para><strong>Payload:</strong></para> | ||
| /// The raw JSON payload from <see cref="OutboxMessage.Payload"/> is published as the message body. | ||
| /// <para><strong>Health Checks:</strong></para> | ||
| /// The <see cref="IsHealthyAsync"/> method verifies that the connection and channel are open. | ||
| /// </remarks> | ||
| internal sealed class RabbitMqMessageTransport : IMessageTransport, IDisposable | ||
| { | ||
| /// <summary>The resolved transport options controlling the RabbitMQ connection and exchange settings.</summary> | ||
| private readonly RabbitMqTransportOptions _options; | ||
|
|
||
| /// <summary>The topic name resolver used to determine the routing key from an outbox message.</summary> | ||
| private readonly ITopicNameResolver _topicNameResolver; | ||
|
|
||
| /// <summary>The RabbitMQ connection adapter.</summary> | ||
| private readonly IRabbitMqConnectionAdapter _connectionAdapter; | ||
|
|
||
| /// <summary>Lazy-initialized RabbitMQ channel for publishing.</summary> | ||
| private IRabbitMqChannelAdapter? _channel; | ||
|
|
||
| /// <summary>Semaphore for thread-safe channel initialization.</summary> | ||
| private readonly SemaphoreSlim _initializationLock = new(1, 1); | ||
|
|
||
| /// <summary>Indicates whether the transport has been disposed.</summary> | ||
| private bool _disposed; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="RabbitMqMessageTransport"/> class. | ||
| /// </summary> | ||
| /// <param name="connectionAdapter">The RabbitMQ connection adapter.</param> | ||
| /// <param name="topicNameResolver">The topic name resolver for determining routing keys from outbox messages.</param> | ||
| /// <param name="options">The transport options.</param> | ||
| internal RabbitMqMessageTransport( | ||
| IRabbitMqConnectionAdapter connectionAdapter, | ||
| ITopicNameResolver topicNameResolver, | ||
| IOptions<RabbitMqTransportOptions> options | ||
| ) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(connectionAdapter); | ||
| ArgumentNullException.ThrowIfNull(topicNameResolver); | ||
| ArgumentNullException.ThrowIfNull(options); | ||
|
|
||
| _connectionAdapter = connectionAdapter; | ||
| _topicNameResolver = topicNameResolver; | ||
| _options = options.Value; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public async Task SendAsync(OutboxMessage message, CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(message); | ||
|
|
||
| var channel = await EnsureChannelAsync(cancellationToken).ConfigureAwait(false); | ||
| var routingKey = ResolveRoutingKey(message); | ||
| var body = Encoding.UTF8.GetBytes(message.Payload); | ||
|
|
||
| var properties = new BasicProperties | ||
| { | ||
| MessageId = message.Id.ToString(), | ||
| CorrelationId = message.CorrelationId, | ||
| ContentType = "application/json", | ||
| Timestamp = new AmqpTimestamp(message.CreatedAt.ToUnixTimeSeconds()), | ||
| Headers = new Dictionary<string, object?> | ||
| { | ||
| ["eventType"] = message.EventType, | ||
| ["retryCount"] = message.RetryCount, | ||
| }, | ||
| }; | ||
|
|
||
| await channel | ||
| .BasicPublishAsync( | ||
| exchange: _options.ExchangeName, | ||
| routingKey: routingKey, | ||
| mandatory: false, | ||
| basicProperties: properties, | ||
| body: body, | ||
| cancellationToken: cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public Task<bool> IsHealthyAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| try | ||
| { | ||
| if (_connectionAdapter?.IsOpen != true || _channel?.IsOpen != true) | ||
| { | ||
| return Task.FromResult(false); | ||
| } | ||
|
|
||
| // Perform a lightweight check by verifying channel is still open | ||
| // RabbitMQ client maintains the connection state internally | ||
| return Task.FromResult(_channel.IsOpen); | ||
| } | ||
| catch (Exception) when (!cancellationToken.IsCancellationRequested) | ||
| { | ||
| return Task.FromResult(false); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Ensures that a channel is available, creating it if necessary. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">A token to monitor for cancellation requests.</param> | ||
| /// <returns>The initialized channel.</returns> | ||
| private async Task<IRabbitMqChannelAdapter> EnsureChannelAsync(CancellationToken cancellationToken) | ||
| { | ||
| if (_channel?.IsOpen == true) | ||
| { | ||
| return _channel; | ||
| } | ||
|
|
||
| await _initializationLock.WaitAsync(cancellationToken).ConfigureAwait(false); | ||
| try | ||
| { | ||
| if (_channel?.IsOpen == true) | ||
| { | ||
| return _channel; | ||
| } | ||
|
|
||
| _channel = await _connectionAdapter.CreateChannelAsync(cancellationToken).ConfigureAwait(false); | ||
|
|
||
|
samtrion marked this conversation as resolved.
|
||
| return _channel; | ||
| } | ||
| finally | ||
| { | ||
| _ = _initializationLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the routing key for a given outbox message. | ||
| /// </summary> | ||
| /// <param name="message">The outbox message to resolve the routing key from.</param> | ||
| /// <returns>The resolved routing key.</returns> | ||
| private string ResolveRoutingKey(OutboxMessage message) => _topicNameResolver.Resolve(message); | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Dispose() | ||
| { | ||
| if (_disposed) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| _channel?.Dispose(); | ||
| _initializationLock.Dispose(); | ||
| _disposed = true; | ||
| } | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
src/NetEvolve.Pulse.RabbitMQ/Outbox/RabbitMqTransportOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| namespace NetEvolve.Pulse.Outbox; | ||
|
|
||
| using NetEvolve.Pulse.Extensibility.Outbox; | ||
|
|
||
| /// <summary> | ||
| /// Configuration options for <see cref="RabbitMqMessageTransport"/>. | ||
| /// </summary> | ||
| public sealed class RabbitMqTransportOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the target exchange name for publishing messages. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This is the RabbitMQ exchange where all outbox messages will be published. | ||
| /// The exchange must already exist; it will not be auto-declared. | ||
| /// </remarks> | ||
| public string ExchangeName { get; set; } = string.Empty; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.