Implemented QueueArgumentsAttribute (untested)

This commit is contained in:
Mark van Renswoude 2022-11-17 16:47:07 +01:00
parent c75f893da8
commit 7143ad3c2f
29 changed files with 281 additions and 133 deletions

View File

@ -24,7 +24,7 @@ namespace ExampleLib
private readonly IDependencyContainer dependencyResolver; private readonly IDependencyContainer dependencyResolver;
private readonly int expectedDoneCount; private readonly int expectedDoneCount;
private int doneCount; private int doneCount;
private readonly TaskCompletionSource<bool> doneSignal = new TaskCompletionSource<bool>(); private readonly TaskCompletionSource<bool> doneSignal = new();
/// <param name="dependencyResolver">Uses Tapeti's IDependencyContainer interface so you can easily switch an example to your favourite IoC container</param> /// <param name="dependencyResolver">Uses Tapeti's IDependencyContainer interface so you can easily switch an example to your favourite IoC container</param>

View File

@ -304,7 +304,7 @@ namespace Tapeti.Flow.Default
private readonly ITapetiConfig config; private readonly ITapetiConfig config;
private readonly FlowProvider flowProvider; private readonly FlowProvider flowProvider;
private readonly List<RequestInfo> requests = new List<RequestInfo>(); private readonly List<RequestInfo> requests = new();
public ParallelRequestBuilder(ITapetiConfig config, FlowProvider flowProvider) public ParallelRequestBuilder(ITapetiConfig config, FlowProvider flowProvider)

View File

@ -29,9 +29,9 @@ namespace Tapeti.Flow.Default
} }
} }
private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new ConcurrentDictionary<Guid, CachedFlowState>(); private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new();
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new ConcurrentDictionary<Guid, Guid>(); private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new();
private readonly LockCollection<Guid> locks = new LockCollection<Guid>(EqualityComparer<Guid>.Default); private readonly LockCollection<Guid> locks = new(EqualityComparer<Guid>.Default);
private HashSet<string> validatedMethods; private HashSet<string> validatedMethods;
private readonly IFlowRepository repository; private readonly IFlowRepository repository;

View File

@ -60,7 +60,7 @@ namespace Tapeti.Flow.FlowHelpers
internal volatile LockItem Next; internal volatile LockItem Next;
private readonly Dictionary<T, LockItem> locks; private readonly Dictionary<T, LockItem> locks;
private readonly TaskCompletionSource<IDisposable> tcs = new TaskCompletionSource<IDisposable>(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource<IDisposable> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly T key; private readonly T key;
public LockItem(Dictionary<T, LockItem> locks, T key) public LockItem(Dictionary<T, LockItem> locks, T key)

View File

@ -18,7 +18,7 @@ namespace Tapeti.Flow.FlowHelpers
} }
private static readonly Regex DeserializeRegex = new Regex("^(?<method>.+?)@(?<assembly>.+?):(?<type>.+?)$"); private static readonly Regex DeserializeRegex = new("^(?<method>.+?)@(?<assembly>.+?):(?<type>.+?)$");
/// <summary> /// <summary>

View File

@ -10,7 +10,7 @@ namespace Tapeti.Serilog.Default
public class DiagnosticContext : IDiagnosticContext public class DiagnosticContext : IDiagnosticContext
{ {
private readonly global::Serilog.ILogger logger; private readonly global::Serilog.ILogger logger;
private readonly List<LogEventProperty> properties = new List<LogEventProperty>(); private readonly List<LogEventProperty> properties = new();
/// <summary> /// <summary>

View File

@ -129,10 +129,11 @@ namespace Tapeti.Serilog
} }
/// <inheritdoc /> /// <inheritdoc />
public void QueueExistsWarning(string queueName, Dictionary<string, string> arguments) public void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments)
{ {
seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({arguments}) and will not be redeclared, queue will be consumed as-is", seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({existingArguments} vs. {arguments}) and will not be redeclared, queue will be consumed as-is",
queueName, queueName,
existingArguments,
arguments); arguments);
} }

View File

@ -8,6 +8,12 @@
<NoWarn>1701;1702</NoWarn> <NoWarn>1701;1702</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Core\**" />
<EmbeddedResource Remove="Core\**" />
<None Remove="Core\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
@ -22,8 +28,4 @@
<ProjectReference Include="..\Tapeti\Tapeti.csproj" /> <ProjectReference Include="..\Tapeti\Tapeti.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Core\" />
</ItemGroup>
</Project> </Project>

View File

@ -33,7 +33,7 @@ namespace Tapeti.Transient
/// <inheritdoc /> /// <inheritdoc />
public async ValueTask Apply(IBindingTarget target) public async ValueTask Apply(IBindingTarget target)
{ {
QueueName = await target.BindDynamicDirect(dynamicQueuePrefix); QueueName = await target.BindDynamicDirect(dynamicQueuePrefix, null);
router.TransientResponseQueueName = QueueName; router.TransientResponseQueueName = QueueName;
} }

View File

@ -13,7 +13,7 @@ namespace Tapeti.Transient
internal class TransientRouter internal class TransientRouter
{ {
private readonly int defaultTimeoutMs; private readonly int defaultTimeoutMs;
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>(); private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new();
/// <summary> /// <summary>
/// The generated name of the dynamic queue to which responses should be sent. /// The generated name of the dynamic queue to which responses should be sent.

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Tapeti.Config namespace Tapeti.Config
@ -80,7 +81,8 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="messageClass">The message class to be bound to the queue</param> /// <param name="messageClass">The message class to be bound to the queue</param>
/// <param name="queueName">The name of the durable queue</param> /// <param name="queueName">The name of the durable queue</param>
ValueTask BindDurable(Type messageClass, string queueName); /// <param name="arguments">Optional arguments</param>
ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Binds the messageClass to a dynamic auto-delete queue. /// Binds the messageClass to a dynamic auto-delete queue.
@ -91,15 +93,17 @@ namespace Tapeti.Config
/// </remarks> /// </remarks>
/// <param name="messageClass">The message class to be bound to the queue</param> /// <param name="messageClass">The message class to be bound to the queue</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamic(Type messageClass, string queuePrefix = null); ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Declares a durable queue but does not add a binding for a messageClass' routing key. /// Declares a durable queue but does not add a binding for a messageClass' routing key.
/// Used for direct-to-queue messages. /// Used for direct-to-queue messages.
/// </summary> /// </summary>
/// <param name="queueName">The name of the durable queue</param> /// <param name="queueName">The name of the durable queue</param>
ValueTask BindDurableDirect(string queueName); /// <param name="arguments">Optional arguments</param>
ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key. /// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
@ -107,16 +111,18 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="messageClass">The message class which will be handled on the queue. It is not actually bound to the queue.</param> /// <param name="messageClass">The message class which will be handled on the queue. It is not actually bound to the queue.</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(Type messageClass = null, string queuePrefix = null); ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key. /// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
/// Used for direct-to-queue messages. Guarantees a unique queue. /// Used for direct-to-queue messages. Guarantees a unique queue.
/// </summary> /// </summary>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(string queuePrefix = null); ValueTask<string> BindDynamicDirect(string queuePrefix, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete /// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete

View File

@ -74,11 +74,11 @@ namespace Tapeti.Connection
/// <summary> /// <summary>
/// Starts a consumer for the specified queue, using the provided bindings to handle messages. /// Starts a consumer for the specified queue, using the provided bindings to handle messages.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName"></param> /// <param name="queueName"></param>
/// <param name="consumer">The consumer implementation which will receive the messages from the queue</param> /// <param name="consumer">The consumer implementation which will receive the messages from the queue</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <returns>The consumer tag as returned by BasicConsume.</returns> /// <returns>The consumer tag as returned by BasicConsume.</returns>
Task<TapetiConsumerTag> Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer); Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Stops the consumer with the specified tag. /// Stops the consumer with the specified tag.
@ -89,40 +89,43 @@ namespace Tapeti.Connection
/// <summary> /// <summary>
/// Creates a durable queue if it does not already exist, and updates the bindings. /// Creates a durable queue if it does not already exist, and updates the bindings.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to create</param> /// <param name="queueName">The name of the queue to create</param>
/// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param> /// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param>
Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable<QueueBinding> bindings); /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Verifies a durable queue exists. Will raise an exception if it does not. /// Verifies a durable queue exists. Will raise an exception if it does not.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to verify</param> /// <param name="queueName">The name of the queue to verify</param>
Task DurableQueueVerify(CancellationToken cancellationToken, string queueName); /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueVerify(string queueName, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Deletes a durable queue. /// Deletes a durable queue.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the queue to delete</param> /// <param name="queueName">The name of the queue to delete</param>
/// <param name="onlyIfEmpty">If true, the queue will only be deleted if it is empty otherwise all bindings will be removed. If false, the queue is deleted even if there are queued messages.</param> /// <param name="onlyIfEmpty">If true, the queue will only be deleted if it is empty otherwise all bindings will be removed. If false, the queue is deleted even if there are queued messages.</param>
Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true); /// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Creates a dynamic queue. /// Creates a dynamic queue.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
Task<string> DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null); /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task<string> DynamicQueueDeclare(string queuePrefix, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Add a binding to a dynamic queue. /// Add a binding to a dynamic queue.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <param name="queueName">The name of the dynamic queue previously created using DynamicQueueDeclare</param> /// <param name="queueName">The name of the dynamic queue previously created using DynamicQueueDeclare</param>
/// <param name="binding">The binding to add to the dynamic queue</param> /// <param name="binding">The binding to add to the dynamic queue</param>
Task DynamicQueueBind(CancellationToken cancellationToken, string queueName, QueueBinding binding); /// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Closes the connection to RabbitMQ gracefully. /// Closes the connection to RabbitMQ gracefully.

View File

@ -20,7 +20,7 @@ namespace Tapeti.Connection
internal class TapetiChannel internal class TapetiChannel
{ {
private readonly Func<IModel> modelFactory; private readonly Func<IModel> modelFactory;
private readonly object taskQueueLock = new object(); private readonly object taskQueueLock = new();
private SingleThreadTaskQueue taskQueue; private SingleThreadTaskQueue taskQueue;
private readonly ModelProvider modelProvider; private readonly ModelProvider modelProvider;

View File

@ -13,6 +13,7 @@ using RabbitMQ.Client.Exceptions;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Default; using Tapeti.Default;
using Tapeti.Exceptions; using Tapeti.Exceptions;
using Tapeti.Helpers;
namespace Tapeti.Connection namespace Tapeti.Connection
{ {
@ -50,7 +51,7 @@ namespace Tapeti.Connection
private readonly HttpClient managementClient; private readonly HttpClient managementClient;
// These fields must be locked using connectionLock // These fields must be locked using connectionLock
private readonly object connectionLock = new object(); private readonly object connectionLock = new();
private long connectionReference; private long connectionReference;
private RabbitMQ.Client.IConnection connection; private RabbitMQ.Client.IConnection connection;
private IModel consumeChannelModel; private IModel consumeChannelModel;
@ -61,12 +62,12 @@ namespace Tapeti.Connection
// These fields are for use in a single TapetiChannel's queue only! // These fields are for use in a single TapetiChannel's queue only!
private ulong lastDeliveryTag; private ulong lastDeliveryTag;
private readonly HashSet<string> deletedQueues = new HashSet<string>(); private readonly HashSet<string> deletedQueues = new();
// These fields must be locked using confirmLock, since the callbacks for BasicAck/BasicReturn can run in a different thread // These fields must be locked using confirmLock, since the callbacks for BasicAck/BasicReturn can run in a different thread
private readonly object confirmLock = new object(); private readonly object confirmLock = new();
private readonly Dictionary<ulong, ConfirmMessageInfo> confirmMessages = new Dictionary<ulong, ConfirmMessageInfo>(); private readonly Dictionary<ulong, ConfirmMessageInfo> confirmMessages = new();
private readonly Dictionary<string, ReturnInfo> returnRoutingKeys = new Dictionary<string, ReturnInfo>(); private readonly Dictionary<string, ReturnInfo> returnRoutingKeys = new();
private class ConfirmMessageInfo private class ConfirmMessageInfo
@ -198,7 +199,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task<TapetiConsumerTag> Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer) public async Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken)
{ {
if (deletedQueues.Contains(queueName)) if (deletedQueues.Contains(queueName))
return null; return null;
@ -285,7 +286,7 @@ namespace Tapeti.Connection
} }
private async Task<bool> GetDurableQueueDeclareRequired(string queueName) private async Task<bool> GetDurableQueueDeclareRequired(string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
var existingQueue = await GetQueueInfo(queueName); var existingQueue = await GetQueueInfo(queueName);
if (existingQueue == null) if (existingQueue == null)
@ -294,18 +295,22 @@ namespace Tapeti.Connection
if (!existingQueue.Durable || existingQueue.AutoDelete || existingQueue.Exclusive) if (!existingQueue.Durable || existingQueue.AutoDelete || existingQueue.Exclusive)
throw new InvalidOperationException($"Durable queue {queueName} already exists with incompatible parameters, durable = {existingQueue.Durable} (expected True), autoDelete = {existingQueue.AutoDelete} (expected False), exclusive = {existingQueue.Exclusive} (expected False)"); throw new InvalidOperationException($"Durable queue {queueName} already exists with incompatible parameters, durable = {existingQueue.Durable} (expected True), autoDelete = {existingQueue.AutoDelete} (expected False), exclusive = {existingQueue.Exclusive} (expected False)");
if (existingQueue.Arguments.Count <= 0) if (arguments == null && existingQueue.Arguments.Count == 0)
return true; return true;
(logger as IBindingLogger)?.QueueExistsWarning(queueName, existingQueue.Arguments); if (existingQueue.Arguments.NullSafeSameValues(arguments))
return true;
(logger as IBindingLogger)?.QueueExistsWarning(queueName, existingQueue.Arguments, arguments);
return false; return false;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable<QueueBinding> bindings) public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{ {
var declareRequired = await GetDurableQueueDeclareRequired(queueName); var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments);
var existingBindings = (await GetQueueBindings(queueName)).ToList(); var existingBindings = (await GetQueueBindings(queueName)).ToList();
var currentBindings = bindings.ToList(); var currentBindings = bindings.ToList();
@ -319,7 +324,7 @@ namespace Tapeti.Connection
if (declareRequired) if (declareRequired)
{ {
bindingLogger?.QueueDeclare(queueName, true, false); bindingLogger?.QueueDeclare(queueName, true, false);
channel.QueueDeclare(queueName, true, false, false); channel.QueueDeclare(queueName, true, false, false, GetDeclareArguments(arguments));
} }
foreach (var binding in currentBindings.Except(existingBindings)) foreach (var binding in currentBindings.Except(existingBindings))
@ -337,10 +342,20 @@ namespace Tapeti.Connection
}); });
} }
/// <inheritdoc />
public async Task DurableQueueVerify(CancellationToken cancellationToken, string queueName) private static IDictionary<string, object> GetDeclareArguments(IReadOnlyDictionary<string, string> arguments)
{ {
if (!await GetDurableQueueDeclareRequired(queueName)) if (arguments == null || arguments.Count == 0)
return null;
return arguments.ToDictionary(p => p.Key, p => (object)Encoding.UTF8.GetBytes(p.Value));
}
/// <inheritdoc />
public async Task DurableQueueVerify(string queueName, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{
if (!await GetDurableQueueDeclareRequired(queueName, arguments))
return; return;
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel => await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
@ -355,7 +370,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true) public async Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken)
{ {
if (!onlyIfEmpty) if (!onlyIfEmpty)
{ {
@ -440,7 +455,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task<string> DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null) public async Task<string> DynamicQueueDeclare(string queuePrefix, IReadOnlyDictionary<string, string> arguments, CancellationToken cancellationToken)
{ {
string queueName = null; string queueName = null;
var bindingLogger = logger as IBindingLogger; var bindingLogger = logger as IBindingLogger;
@ -454,11 +469,11 @@ namespace Tapeti.Connection
{ {
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N"); queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
bindingLogger?.QueueDeclare(queueName, false, false); bindingLogger?.QueueDeclare(queueName, false, false);
channel.QueueDeclare(queueName); channel.QueueDeclare(queueName, arguments: GetDeclareArguments(arguments));
} }
else else
{ {
queueName = channel.QueueDeclare().QueueName; queueName = channel.QueueDeclare(arguments: GetDeclareArguments(arguments)).QueueName;
bindingLogger?.QueueDeclare(queueName, false, false); bindingLogger?.QueueDeclare(queueName, false, false);
} }
}); });
@ -467,7 +482,7 @@ namespace Tapeti.Connection
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DynamicQueueBind(CancellationToken cancellationToken, string queueName, QueueBinding binding) public async Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken)
{ {
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel => await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
{ {
@ -523,7 +538,7 @@ namespace Tapeti.Connection
} }
private static readonly List<HttpStatusCode> TransientStatusCodes = new List<HttpStatusCode>() private static readonly List<HttpStatusCode> TransientStatusCodes = new()
{ {
HttpStatusCode.GatewayTimeout, HttpStatusCode.GatewayTimeout,
HttpStatusCode.RequestTimeout, HttpStatusCode.RequestTimeout,
@ -675,7 +690,7 @@ namespace Tapeti.Connection
} }
private readonly HashSet<string> declaredExchanges = new HashSet<string>(); private readonly HashSet<string> declaredExchanges = new();
private void DeclareExchange(IModel channel, string exchange) private void DeclareExchange(IModel channel, string exchange)
{ {
@ -842,7 +857,7 @@ namespace Tapeti.Connection
GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(_ => { }); GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(_ => { });
}; };
capturedPublishChannelModel.ModelShutdown += (sender, args) => capturedPublishChannelModel.ModelShutdown += (_, _) =>
{ {
lock (connectionLock) lock (connectionLock)
{ {

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Helpers;
namespace Tapeti.Connection namespace Tapeti.Connection
{ {
@ -13,7 +14,7 @@ namespace Tapeti.Connection
private readonly Func<ITapetiClient> clientFactory; private readonly Func<ITapetiClient> clientFactory;
private readonly ITapetiConfig config; private readonly ITapetiConfig config;
private bool consuming; private bool consuming;
private readonly List<TapetiConsumerTag> consumerTags = new List<TapetiConsumerTag>(); private readonly List<TapetiConsumerTag> consumerTags = new();
private CancellationTokenSource initializeCancellationTokenSource; private CancellationTokenSource initializeCancellationTokenSource;
@ -149,7 +150,7 @@ namespace Tapeti.Connection
var queueName = group.Key; var queueName = group.Key;
var consumer = new TapetiConsumer(cancellationToken, config, queueName, group); var consumer = new TapetiConsumer(cancellationToken, config, queueName, group);
return await clientFactory().Consume(cancellationToken, queueName, consumer); return await clientFactory().Consume(queueName, consumer, cancellationToken);
}))).Where(t => t != null)); }))).Where(t => t != null));
} }
@ -165,9 +166,10 @@ namespace Tapeti.Connection
{ {
public string QueueName; public string QueueName;
public List<Type> MessageClasses; public List<Type> MessageClasses;
public IReadOnlyDictionary<string, string> Arguments;
} }
private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new Dictionary<string, List<DynamicQueueInfo>>(); private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new();
protected CustomBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) protected CustomBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken)
@ -185,38 +187,38 @@ namespace Tapeti.Connection
} }
public abstract ValueTask BindDurable(Type messageClass, string queueName); public abstract ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments);
public abstract ValueTask BindDurableDirect(string queueName); public abstract ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments);
public abstract ValueTask BindDurableObsolete(string queueName); public abstract ValueTask BindDurableObsolete(string queueName);
public async ValueTask<string> BindDynamic(Type messageClass, string queuePrefix = null) public async ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{ {
var result = await DeclareDynamicQueue(messageClass, queuePrefix); var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
if (!result.IsNewMessageClass) if (!result.IsNewMessageClass)
return result.QueueName; return result.QueueName;
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass); var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
var exchange = ExchangeStrategy.GetExchange(messageClass); var exchange = ExchangeStrategy.GetExchange(messageClass);
await ClientFactory().DynamicQueueBind(CancellationToken, result.QueueName, new QueueBinding(exchange, routingKey)); await ClientFactory().DynamicQueueBind(result.QueueName, new QueueBinding(exchange, routingKey), CancellationToken);
return result.QueueName; return result.QueueName;
} }
public async ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix = null) public async ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{ {
var result = await DeclareDynamicQueue(messageClass, queuePrefix); var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
return result.QueueName; return result.QueueName;
} }
public async ValueTask<string> BindDynamicDirect(string queuePrefix = null) public async ValueTask<string> BindDynamicDirect(string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{ {
// If we don't know the routing key, always create a new queue to ensure there is no overlap. // If we don't know the routing key, always create a new queue to ensure there is no overlap.
// Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either. // Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either.
return await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix); return await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
} }
@ -226,7 +228,7 @@ namespace Tapeti.Connection
public bool IsNewMessageClass; public bool IsNewMessageClass;
} }
private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix) private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix, IReadOnlyDictionary<string, string> arguments)
{ {
// Group by prefix // Group by prefix
var key = queuePrefix ?? ""; var key = queuePrefix ?? "";
@ -241,7 +243,7 @@ namespace Tapeti.Connection
foreach (var existingQueueInfo in prefixQueues) foreach (var existingQueueInfo in prefixQueues)
{ {
// ReSharper disable once InvertIf // ReSharper disable once InvertIf
if (!existingQueueInfo.MessageClasses.Contains(messageClass)) if (!existingQueueInfo.MessageClasses.Contains(messageClass) && existingQueueInfo.Arguments.NullSafeSameValues(arguments))
{ {
// Allow this routing key in the existing dynamic queue // Allow this routing key in the existing dynamic queue
var result = new DeclareDynamicQueueResult var result = new DeclareDynamicQueueResult
@ -258,11 +260,12 @@ namespace Tapeti.Connection
} }
// Declare a new queue // Declare a new queue
var queueName = await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix); var queueName = await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
var queueInfo = new DynamicQueueInfo var queueInfo = new DynamicQueueInfo
{ {
QueueName = queueName, QueueName = queueName,
MessageClasses = new List<Type> { messageClass } MessageClasses = new List<Type> { messageClass },
Arguments = arguments
}; };
prefixQueues.Add(queueInfo); prefixQueues.Add(queueInfo);
@ -278,8 +281,15 @@ namespace Tapeti.Connection
private class DeclareDurableQueuesBindingTarget : CustomBindingTarget private class DeclareDurableQueuesBindingTarget : CustomBindingTarget
{ {
private readonly Dictionary<string, List<Type>> durableQueues = new Dictionary<string, List<Type>>(); private struct DurableQueueInfo
private readonly HashSet<string> obsoleteDurableQueues = new HashSet<string>(); {
public List<Type> MessageClasses;
public IReadOnlyDictionary<string, string> Arguments;
}
private readonly Dictionary<string, DurableQueueInfo> durableQueues = new();
private readonly HashSet<string> obsoleteDurableQueues = new();
public DeclareDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken) public DeclareDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken)
@ -287,29 +297,50 @@ namespace Tapeti.Connection
} }
public override ValueTask BindDurable(Type messageClass, string queueName) public override ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
// Collect the message classes per queue so we can determine afterwards // Collect the message classes per queue so we can determine afterwards
// if any of the bindings currently set on the durable queue are no // if any of the bindings currently set on the durable queue are no
// longer valid and should be removed. // longer valid and should be removed.
if (!durableQueues.TryGetValue(queueName, out var messageClasses)) if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
{ {
durableQueues.Add(queueName, new List<Type> durableQueues.Add(queueName, new DurableQueueInfo
{ {
messageClass MessageClasses = new List<Type>
{
messageClass
},
Arguments = arguments
}); });
} }
else if (!messageClasses.Contains(messageClass)) else
messageClasses.Add(messageClass); {
if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
if (!durableQueueInfo.MessageClasses.Contains(messageClass))
durableQueueInfo.MessageClasses.Add(messageClass);
}
return default; return default;
} }
public override ValueTask BindDurableDirect(string queueName) public override ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
if (!durableQueues.ContainsKey(queueName)) if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
durableQueues.Add(queueName, new List<Type>()); {
durableQueues.Add(queueName, new DurableQueueInfo
{
MessageClasses = new List<Type>(),
Arguments = arguments
});
}
else
{
if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
}
return default; return default;
} }
@ -334,7 +365,7 @@ namespace Tapeti.Connection
{ {
await Task.WhenAll(durableQueues.Select(async queue => await Task.WhenAll(durableQueues.Select(async queue =>
{ {
var bindings = queue.Value.Select(messageClass => var bindings = queue.Value.MessageClasses.Select(messageClass =>
{ {
var exchange = ExchangeStrategy.GetExchange(messageClass); var exchange = ExchangeStrategy.GetExchange(messageClass);
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass); var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
@ -342,7 +373,7 @@ namespace Tapeti.Connection
return new QueueBinding(exchange, routingKey); return new QueueBinding(exchange, routingKey);
}); });
await client.DurableQueueDeclare(CancellationToken, queue.Key, bindings); await client.DurableQueueDeclare(queue.Key, bindings, queue.Value.Arguments, CancellationToken);
})); }));
} }
@ -351,7 +382,7 @@ namespace Tapeti.Connection
{ {
await Task.WhenAll(obsoleteDurableQueues.Except(durableQueues.Keys).Select(async queue => await Task.WhenAll(obsoleteDurableQueues.Except(durableQueues.Keys).Select(async queue =>
{ {
await client.DurableQueueDelete(CancellationToken, queue); await client.DurableQueueDelete(queue, true, CancellationToken);
})); }));
} }
} }
@ -359,7 +390,7 @@ namespace Tapeti.Connection
private class PassiveDurableQueuesBindingTarget : CustomBindingTarget private class PassiveDurableQueuesBindingTarget : CustomBindingTarget
{ {
private readonly HashSet<string> durableQueues = new HashSet<string>(); private readonly HashSet<string> durableQueues = new();
public PassiveDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken) public PassiveDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken)
@ -367,14 +398,14 @@ namespace Tapeti.Connection
} }
public override async ValueTask BindDurable(Type messageClass, string queueName) public override async ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
await VerifyDurableQueue(queueName); await VerifyDurableQueue(queueName, arguments);
} }
public override async ValueTask BindDurableDirect(string queueName) public override async ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
await VerifyDurableQueue(queueName); await VerifyDurableQueue(queueName, arguments);
} }
public override ValueTask BindDurableObsolete(string queueName) public override ValueTask BindDurableObsolete(string queueName)
@ -383,12 +414,12 @@ namespace Tapeti.Connection
} }
private async Task VerifyDurableQueue(string queueName) private async Task VerifyDurableQueue(string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
if (!durableQueues.Add(queueName)) if (!durableQueues.Add(queueName))
return; return;
await ClientFactory().DurableQueueVerify(CancellationToken, queueName); await ClientFactory().DurableQueueVerify(queueName, arguments, CancellationToken);
} }
} }
@ -400,12 +431,12 @@ namespace Tapeti.Connection
} }
public override ValueTask BindDurable(Type messageClass, string queueName) public override ValueTask BindDurable(Type messageClass, string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
return default; return default;
} }
public override ValueTask BindDurableDirect(string queueName) public override ValueTask BindDurableDirect(string queueName, IReadOnlyDictionary<string, string> arguments)
{ {
return default; return default;
} }

View File

@ -81,7 +81,13 @@ namespace Tapeti.Default
} }
/// <inheritdoc /> /// <inheritdoc />
public void QueueExistsWarning(string queueName, Dictionary<string, string> arguments) public void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments)
{
Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({GetArgumentsText(existingArguments)} vs. {GetArgumentsText(arguments)}) and will not be redeclared, queue will be consumed as-is");
}
private static string GetArgumentsText(IReadOnlyDictionary<string, string> arguments)
{ {
var argumentsText = new StringBuilder(); var argumentsText = new StringBuilder();
foreach (var pair in arguments) foreach (var pair in arguments)
@ -91,10 +97,11 @@ namespace Tapeti.Default
argumentsText.Append($"{pair.Key} = {pair.Value}"); argumentsText.Append($"{pair.Key} = {pair.Value}");
} }
Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({argumentsText}) and will not be redeclared, queue will be consumed as-is"); return argumentsText.ToString();
} }
/// <inheritdoc /> /// <inheritdoc />
public void QueueBind(string queueName, bool durable, string exchange, string routingKey) public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
{ {

View File

@ -9,7 +9,7 @@ namespace Tapeti.Default
internal class ControllerBindingContext : IControllerBindingContext internal class ControllerBindingContext : IControllerBindingContext
{ {
private BindingTargetMode? bindingTargetMode; private BindingTargetMode? bindingTargetMode;
private readonly List<IControllerMiddlewareBase> middleware = new List<IControllerMiddlewareBase>(); private readonly List<IControllerMiddlewareBase> middleware = new();
private readonly List<ControllerBindingParameter> parameters; private readonly List<ControllerBindingParameter> parameters;
private readonly ControllerBindingResult result; private readonly ControllerBindingResult result;

View File

@ -117,10 +117,10 @@ namespace Tapeti.Default
{ {
case BindingTargetMode.Default: case BindingTargetMode.Default:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic) if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name); QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else else
{ {
await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name); await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name; QueueName = bindingInfo.QueueInfo.Name;
} }
@ -128,10 +128,10 @@ namespace Tapeti.Default
case BindingTargetMode.Direct: case BindingTargetMode.Direct:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic) if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name); QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else else
{ {
await target.BindDurableDirect(bindingInfo.QueueInfo.Name); await target.BindDurableDirect(bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name; QueueName = bindingInfo.QueueInfo.Name;
} }
@ -316,7 +316,11 @@ namespace Tapeti.Default
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// Optional arguments (x-arguments) passed when declaring the queue.
/// </summary>
public IReadOnlyDictionary<string, string> QueueArguments { get; set; }
/// <summary> /// <summary>
/// Determines if the QueueInfo properties contain a valid combination. /// Determines if the QueueInfo properties contain a valid combination.
/// </summary> /// </summary>

View File

@ -16,8 +16,8 @@ namespace Tapeti.Default
private const string ClassTypeHeader = "classType"; private const string ClassTypeHeader = "classType";
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>(); private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new();
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>(); private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new();
private readonly JsonSerializerSettings serializerSettings; private readonly JsonSerializerSettings serializerSettings;

View File

@ -8,7 +8,7 @@ namespace Tapeti.Default
{ {
internal class MessageContext : IMessageContext internal class MessageContext : IMessageContext
{ {
private readonly Dictionary<Type, IMessageContextPayload> payloads = new Dictionary<Type, IMessageContextPayload>(); private readonly Dictionary<Type, IMessageContextPayload> payloads = new();
/// <inheritdoc /> /// <inheritdoc />
@ -117,7 +117,7 @@ namespace Tapeti.Default
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public class KeyValuePayload : IMessageContextPayload, IDisposable, IAsyncDisposable public class KeyValuePayload : IMessageContextPayload, IDisposable, IAsyncDisposable
{ {
private readonly Dictionary<string, object> items = new Dictionary<string, object>(); private readonly Dictionary<string, object> items = new();
public KeyValuePayload(string key, object value) public KeyValuePayload(string key, object value)

View File

@ -10,7 +10,7 @@ namespace Tapeti.Default
/// </summary> /// </summary>
public class MessageProperties : IMessageProperties public class MessageProperties : IMessageProperties
{ {
private readonly Dictionary<string, string> headers = new Dictionary<string, string>(); private readonly Dictionary<string, string> headers = new();
/// <inheritdoc /> /// <inheritdoc />

View File

@ -13,7 +13,7 @@ namespace Tapeti.Default
/// </example> /// </example>
public class NamespaceMatchExchangeStrategy : IExchangeStrategy public class NamespaceMatchExchangeStrategy : IExchangeStrategy
{ {
private static readonly Regex NamespaceRegex = new Regex("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex NamespaceRegex = new("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
/// <inheritdoc /> /// <inheritdoc />

View File

@ -28,9 +28,9 @@ namespace Tapeti.Default
(?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z]) (?(?<=[A-Z])[A-Z](?=[a-z])|[A-Z])
)"; )";
private static readonly Regex SeparatorRegex = new Regex(SeparatorPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); private static readonly Regex SeparatorRegex = new(SeparatorPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new ConcurrentDictionary<Type, string>(); private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new();
/// <inheritdoc /> /// <inheritdoc />

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace Tapeti.Helpers
{
/// <summary>
/// Provides extension methods for dictionaries.
/// </summary>
public static class DictionaryHelper
{
/// <summary>
/// Checks if two dictionaries are considered compatible. If either is null they are considered empty.
/// </summary>
public static bool NullSafeSameValues(this IReadOnlyDictionary<string, string> arguments1, IReadOnlyDictionary<string, string> arguments2)
{
if (arguments1 == null || arguments2 == null)
return (arguments1 == null || arguments1.Count == 0) && (arguments2 == null || arguments2.Count == 0);
if (arguments1.Count != arguments2.Count)
return false;
foreach (var pair in arguments1)
{
if (!arguments2.TryGetValue(pair.Key, out var value2) || value2 != arguments1[pair.Key])
return false;
}
return true;
}
}
}

View File

@ -138,8 +138,9 @@ namespace Tapeti
/// If the queue already exists but should be compatible QueueDeclare will be called instead. /// If the queue already exists but should be compatible QueueDeclare will be called instead.
/// </summary> /// </summary>
/// <param name="queueName">The name of the queue that is declared</param> /// <param name="queueName">The name of the queue that is declared</param>
/// <param name="arguments">The x-arguments of the existing queue</param> /// <param name="existingArguments">The x-arguments of the existing queue</param>
void QueueExistsWarning(string queueName, Dictionary<string, string> arguments); /// <param name="arguments">The x-arguments of the queue that would be declared</param>
void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments);
/// <summary> /// <summary>
/// Called before a binding is added to a queue. /// Called before a binding is added to a queue.

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks> <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
@ -42,6 +42,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Tapeti.Annotations" Version="3.0.0" /> <PackageReference Include="Tapeti.Annotations" Version="3.*-*" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -18,7 +18,7 @@ namespace Tapeti
public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess
{ {
private Config config; private Config config;
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>(); private readonly List<IControllerBindingMiddleware> bindingMiddleware = new();
/// <inheritdoc /> /// <inheritdoc />
@ -225,9 +225,9 @@ namespace Tapeti
/// <inheritdoc /> /// <inheritdoc />
internal class Config : ITapetiConfig internal class Config : ITapetiConfig
{ {
private readonly ConfigFeatures features = new ConfigFeatures(); private readonly ConfigFeatures features = new();
private readonly ConfigMiddleware middleware = new ConfigMiddleware(); private readonly ConfigMiddleware middleware = new();
private readonly ConfigBindings bindings = new ConfigBindings(); private readonly ConfigBindings bindings = new();
public IDependencyResolver DependencyResolver { get; } public IDependencyResolver DependencyResolver { get; }
public ITapetiConfigFeatues Features => features; public ITapetiConfigFeatues Features => features;
@ -291,8 +291,8 @@ namespace Tapeti
internal class ConfigMiddleware : ITapetiConfigMiddleware internal class ConfigMiddleware : ITapetiConfigMiddleware
{ {
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>(); private readonly List<IMessageMiddleware> messageMiddleware = new();
private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>(); private readonly List<IPublishMiddleware> publishMiddleware = new();
public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware; public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware;

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Tapeti.Annotations; using Tapeti.Annotations;
@ -79,7 +80,7 @@ namespace Tapeti
} }
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo; var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
if (!(methodQueueInfo is { IsValid: true })) if (methodQueueInfo is not { IsValid: true })
throw new TopologyConfigurationException( throw new TopologyConfigurationException(
$"Method {method.Name} or controller {controller.Name} requires a queue attribute"); $"Method {method.Name} or controller {controller.Name} requires a queue attribute");
@ -136,12 +137,59 @@ namespace Tapeti
if (dynamicQueueAttribute != null && durableQueueAttribute != null) if (dynamicQueueAttribute != null && durableQueueAttribute != null)
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}"); throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
var queueArgumentsAttribute = member.GetCustomAttribute<QueueArgumentsAttribute>();
if (dynamicQueueAttribute != null) if (dynamicQueueAttribute != null)
return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix }; return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix, QueueArguments = GetQueueArguments(queueArgumentsAttribute) };
return durableQueueAttribute != null return durableQueueAttribute != null
? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name } ? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name, QueueArguments = GetQueueArguments(queueArgumentsAttribute) }
: null; : null;
} }
private static IReadOnlyDictionary<string, string> GetQueueArguments(QueueArgumentsAttribute queueArgumentsAttribute)
{
if (queueArgumentsAttribute == null)
return null;
#if NETSTANDARD2_1_OR_GREATER
var arguments = new Dictionary<string, string>(queueArgumentsAttribute.CustomArguments);
#else
var arguments = new Dictionary<string, string>();
foreach (var pair in queueArgumentsAttribute.CustomArguments)
arguments.Add(pair.Key, pair.Value);
#endif
if (queueArgumentsAttribute.MaxLength > 0)
arguments.Add(@"x-max-length", queueArgumentsAttribute.MaxLength.ToString());
if (queueArgumentsAttribute.MaxLengthBytes > 0)
arguments.Add(@"x-max-length-bytes", queueArgumentsAttribute.MaxLengthBytes.ToString());
if (queueArgumentsAttribute.MessageTTL > 0)
arguments.Add(@"x-message-ttl", queueArgumentsAttribute.MessageTTL.ToString());
switch (queueArgumentsAttribute.Overflow)
{
case RabbitMQOverflow.NotSpecified:
break;
case RabbitMQOverflow.DropHead:
arguments.Add(@"x-overflow", @"drop-head");
break;
case RabbitMQOverflow.RejectPublish:
arguments.Add(@"x-overflow", @"reject-publish");
break;
case RabbitMQOverflow.RejectPublishDeadletter:
arguments.Add(@"x-overflow", @"reject-publish-dlx");
break;
default:
throw new ArgumentOutOfRangeException(nameof(queueArgumentsAttribute.Overflow), queueArgumentsAttribute.Overflow, "Unsupported Overflow value");
}
return arguments.Count > 0 ? arguments : null;
}
} }
} }

View File

@ -12,10 +12,10 @@ namespace Tapeti.Tasks
/// </summary> /// </summary>
public class SingleThreadTaskQueue : IDisposable public class SingleThreadTaskQueue : IDisposable
{ {
private readonly object previousTaskLock = new object(); private readonly object previousTaskLock = new();
private Task previousTask = Task.CompletedTask; private Task previousTask = Task.CompletedTask;
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new Lazy<SingleThreadTaskScheduler>(); private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new();
/// <summary> /// <summary>
@ -70,7 +70,7 @@ namespace Tapeti.Tasks
public override int MaximumConcurrencyLevel => 1; public override int MaximumConcurrencyLevel => 1;
private readonly Queue<Task> scheduledTasks = new Queue<Task>(); private readonly Queue<Task> scheduledTasks = new();
private bool disposed; private bool disposed;