Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:

- name: Build
run: dotnet build src --configuration Release --no-restore

- name: Test
run: dotnet test tests/Linn.Common.Messaging.RabbitMQ.Tests --configuration Release

- name: Pack
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Expand Down
23 changes: 22 additions & 1 deletion Linn.Common.Messaging.RabbitMQ.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35506.116 d17.12
VisualStudioVersion = 17.12.35506.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linn.Common.Messaging.RabbitMQ", "src\Linn.Common.Messaging.RabbitMQ.csproj", "{C58B1BE7-9DAD-4F3A-AF6E-B9515DE0612B}"
EndProject
Expand All @@ -14,6 +14,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linn.Common.Messaging.RabbitMQ.Tests", "tests\Linn.Common.Messaging.RabbitMQ.Tests\Linn.Common.Messaging.RabbitMQ.Tests.csproj", "{5F62DC6D-9E1E-4932-B350-926F444EDAA4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -36,8 +42,23 @@ Global
{C58B1BE7-9DAD-4F3A-AF6E-B9515DE0612B}.Release|x64.Build.0 = Release|Any CPU
{C58B1BE7-9DAD-4F3A-AF6E-B9515DE0612B}.Release|x86.ActiveCfg = Release|Any CPU
{C58B1BE7-9DAD-4F3A-AF6E-B9515DE0612B}.Release|x86.Build.0 = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|x64.ActiveCfg = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|x64.Build.0 = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|x86.ActiveCfg = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Debug|x86.Build.0 = Debug|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|Any CPU.Build.0 = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|x64.ActiveCfg = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|x64.Build.0 = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|x86.ActiveCfg = Release|Any CPU
{5F62DC6D-9E1E-4932-B350-926F444EDAA4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5F62DC6D-9E1E-4932-B350-926F444EDAA4} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ var publisher = new JsonMessagePublisher<OrderDto>(
await publisher.PublishAsync(new OrderDto { Id = 42, Total = 99.99m });
```

`JsonMessagePublisher<T>` serialises `T` to UTF-8 JSON and sends it to the exchange with the configured routing key. Optional `headers` and `JsonSerializerOptions` can be supplied to the constructor.
`JsonMessagePublisher<T>` serialises `T` to UTF-8 JSON and sends it to the exchange with the configured routing key. Optional `headers` and `JsonSerializerOptions` can be supplied to the constructor; set the `ContentType` and `Persistent` (delivery mode 2) init properties for those message properties, e.g. `new JsonMessagePublisher<OrderDto>(publisher, "order.created") { ContentType = "application/vnd.example+json", Persistent = true }`. `RabbitPublisher` applies `Message.ContentType` and `Message.DeliveryMode` (2 = persistent, 1 = transient) to the published properties.

---

Expand Down Expand Up @@ -194,6 +194,11 @@ var config = new RabbitChannelConfiguration(
| `headers` | `IReadOnlyDictionary<string, object>?` | `null` | Optional message headers |
| `serializerOptions` | `JsonSerializerOptions?` | `null` | Custom JSON serialisation options |

| Init property | Type | Default | Description |
|---|---|---|---|
| `ContentType` | `string?` | `null` | Content type set on published messages |
| `Persistent` | `bool` | `false` | Publish with delivery mode 2 (persistent) |

### `ExchangeBinding`

```csharp
Expand Down
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [6.1.0] - 2026-10-02
### *Bug Fix*
- RabbitPublisher now sets ContentType and DeliveryMode from Message (previously ignored). DeliveryMode 2 maps to Persistent, 1 to Transient; null leaves the library default.
### *Changes*
- JsonMessagePublisher<T> gains `ContentType` and `Persistent` init properties. The constructor signature is unchanged, so this is binary and source compatible.
- CI now runs the test project.

## [6.0.1] - 2026-08-25
### *Bug Fix*
- Remove `x-dead-letter-routing-key` from main queue arguments. Previously set to `""`, this caused RabbitMQ to overwrite the original routing key on dead-letter, making messages unroutable when requeued from the DLQ back to the original exchange. The original routing key is now preserved automatically.
Expand Down
12 changes: 11 additions & 1 deletion src/JsonMessagePublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@

namespace Linn.Common.Messaging.RabbitMQ;

// The constructor signature is unchanged since 5.1 (binary compatibility for assemblies compiled
// against earlier versions); newer options are init properties.
public class JsonMessagePublisher<T>(
RabbitPublisher rabbitPublisher,
string routingKey,
IReadOnlyDictionary<string, object>? headers = null,
JsonSerializerOptions? serializerOptions = null)
: IPublisher<T>
{
/// <summary>Content type set on published messages (e.g. a vendor media type). Default: none.</summary>
public string? ContentType { get; init; }

/// <summary>Publish with delivery mode 2 (persistent). Default: false (broker default).</summary>
public bool Persistent { get; init; }

public async Task PublishAsync(T obj, CancellationToken cancellationToken = default)
{
var msg = new Message
{
RoutingKey = routingKey,
Body = JsonSerializer.SerializeToUtf8Bytes(obj, serializerOptions),
Headers = headers ?? new Dictionary<string, object>()
Headers = headers ?? new Dictionary<string, object>(),
ContentType = this.ContentType,
DeliveryMode = this.Persistent ? 2 : null
};

await rabbitPublisher.PublishAsync(msg, cancellationToken);
Expand Down
2 changes: 1 addition & 1 deletion src/Linn.Common.Messaging.RabbitMQ.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>Linn.Common.Messaging.RabbitMQ</AssemblyName>
<PackageId>Linn.Common.Messaging.RabbitMQ</PackageId>
<Version>6.0.1</Version>
<Version>6.1.0</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
Expand Down
14 changes: 14 additions & 0 deletions src/RabbitMessagePublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ public async Task PublishAsync(Message message, CancellationToken cancellationTo
? (IDictionary<string, object?>?)message.Headers : null
};

if (message.ContentType != null)
{
props.ContentType = message.ContentType;
}

if (message.DeliveryMode == 2)
{
props.DeliveryMode = DeliveryModes.Persistent;
}
else if (message.DeliveryMode == 1)
{
props.DeliveryMode = DeliveryModes.Transient;
}

await channel.BasicPublishAsync(
exchange: exchange,
routingKey: message.RoutingKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Linn.Common.Messaging.RabbitMQ.csproj" />
</ItemGroup>
</Project>
102 changes: 102 additions & 0 deletions tests/Linn.Common.Messaging.RabbitMQ.Tests/RabbitPublisherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Text;
using FluentAssertions;
using Linn.Common.Messaging.RabbitMQ;
using NSubstitute;
using NUnit.Framework;
using RabbitMQ.Client;

namespace Linn.Common.Messaging.RabbitMQ.Tests;

public class RabbitPublisherTests
{
private IChannel channel = null!;
private BasicProperties? captured;

[SetUp]
public void SetUp()
{
this.channel = Substitute.For<IChannel>();
this.captured = null;
this.channel.BasicPublishAsync(
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Do<BasicProperties>(p => this.captured = p),
Arg.Any<ReadOnlyMemory<byte>>(),
Arg.Any<CancellationToken>()).Returns(ValueTask.CompletedTask);
}

private Task Publish(Message m) => new RabbitPublisher(this.channel, "ex").PublishAsync(m);

private static Message Msg(string? contentType, int? mode) => new()
{
RoutingKey = "rk",
Body = Encoding.UTF8.GetBytes("{}"),
ContentType = contentType,
DeliveryMode = mode
};

[Test]
public async Task ShouldSetContentTypeAndPersistent()
{
await this.Publish(Msg("application/json", 2));
this.captured.Should().NotBeNull();
this.captured!.ContentType.Should().Be("application/json");
this.captured.DeliveryMode.Should().Be(DeliveryModes.Persistent);
}

[Test]
public async Task ShouldSetTransient()
{
await this.Publish(Msg(null, 1));
this.captured!.DeliveryMode.Should().Be(DeliveryModes.Transient);
}

[Test]
public async Task ShouldLeaveUnsetWhenNull()
{
await this.Publish(Msg(null, null));
this.captured!.ContentType.Should().BeNull();
this.captured.IsDeliveryModePresent().Should().BeFalse();
this.captured.IsContentTypePresent().Should().BeFalse();
}

[Test]
public async Task ShouldKeepHeaders()
{
var m = new Message
{
RoutingKey = "rk",
Headers = new Dictionary<string, object> { ["a"] = "b" }
};
await this.Publish(m);
this.captured!.Headers.Should().ContainKey("a");
}

[Test]
public async Task JsonPublisherPassesContentTypeAndPersistence()
{
var p = new JsonMessagePublisher<string>(new RabbitPublisher(this.channel, "ex"), "rk")
{
ContentType = "application/json",
Persistent = true
};
await p.PublishAsync("x");
this.captured!.ContentType.Should().Be("application/json");
this.captured.DeliveryMode.Should().Be(DeliveryModes.Persistent);
}

// Binary compatibility: assemblies compiled against 5.1-6.0 call this exact constructor.
[Test]
public void JsonPublisherKeepsItsOriginalConstructorSignature()
{
var ctor = typeof(JsonMessagePublisher<string>).GetConstructor(
new[]
{
typeof(RabbitPublisher), typeof(string), typeof(IReadOnlyDictionary<string, object>),
typeof(System.Text.Json.JsonSerializerOptions)
});
ctor.Should().NotBeNull();
typeof(JsonMessagePublisher<string>).GetConstructors().Should().HaveCount(1);
}
}
Loading