diff --git a/Examples/01-PublishSubscribe/01-PublishSubscribe.csproj b/Examples/01-PublishSubscribe/01-PublishSubscribe.csproj
index 707f649..a4a9a3c 100644
--- a/Examples/01-PublishSubscribe/01-PublishSubscribe.csproj
+++ b/Examples/01-PublishSubscribe/01-PublishSubscribe.csproj
@@ -2,16 +2,16 @@
Exe
- net5.0
+ net6.0
_01_PublishSubscribe
+ enable
-
-
-
-
-
+
+
+
+
@@ -20,7 +20,6 @@
-
diff --git a/Examples/01-PublishSubscribe/Program.cs b/Examples/01-PublishSubscribe/Program.cs
index 295de7b..89c1f9b 100644
--- a/Examples/01-PublishSubscribe/Program.cs
+++ b/Examples/01-PublishSubscribe/Program.cs
@@ -13,8 +13,6 @@ using Tapeti.DataAnnotations;
using Tapeti.Default;
using Tapeti.Ninject;
using Tapeti.SimpleInjector;
-using Tapeti.UnityContainer;
-using Unity;
using Container = SimpleInjector.Container;
// ReSharper disable UnusedMember.Global
@@ -23,14 +21,13 @@ namespace _01_PublishSubscribe
{
public class Program
{
- public static void Main(string[] args)
+ public static void Main()
{
var dependencyResolver = GetSimpleInjectorDependencyResolver();
// or use your IoC container of choice:
//var dependencyResolver = GetAutofacDependencyResolver();
//var dependencyResolver = GetCastleWindsorDependencyResolver();
- //var dependencyResolver = GetUnityDependencyResolver();
//var dependencyResolver = GetNinjectDependencyResolver();
// This helper is used because this example is not run as a service. You do not
@@ -47,7 +44,7 @@ namespace _01_PublishSubscribe
.RegisterAllControllers()
.Build();
- using (var connection = new TapetiConnection(config)
+ await using var connection = new TapetiConnection(config)
{
// Params is optional if you want to use the defaults, but we'll set it
// explicitly for this example
@@ -63,28 +60,27 @@ namespace _01_PublishSubscribe
{ "example", "01 - Publish Subscribe" }
}
}
- })
- {
- // IoC containers that separate the builder from the resolver (Autofac) must be built after
- // creating a TapetConnection, as it modifies the container by injecting IPublisher.
- (dependencyResolver as AutofacDependencyResolver)?.Build();
+ };
+
+ // IoC containers that separate the builder from the resolver (Autofac) must be built after
+ // creating a TapetConnection, as it modifies the container by injecting IPublisher.
+ (dependencyResolver as AutofacDependencyResolver)?.Build();
- // Create the queues and start consuming immediately.
- // If you need to do some processing before processing messages, but after the
- // queues have initialized, pass false as the startConsuming parameter and store
- // the returned ISubscriber. Then call Resume on it later.
- await connection.Subscribe();
+ // Create the queues and start consuming immediately.
+ // If you need to do some processing before processing messages, but after the
+ // queues have initialized, pass false as the startConsuming parameter and store
+ // the returned ISubscriber. Then call Resume on it later.
+ await connection.Subscribe();
- // We could get an IPublisher from the container directly, but since you'll usually use
- // it as an injected constructor parameter this shows
- await dependencyResolver.Resolve().SendTestMessage();
+ // We could get an IPublisher from the container directly, but since you'll usually use
+ // it as an injected constructor parameter this shows
+ await dependencyResolver.Resolve().SendTestMessage();
- // Wait for the controller to signal that the message has been received
- await waitForDone();
- }
+ // Wait for the controller to signal that the message has been received
+ await waitForDone();
}
@@ -132,17 +128,6 @@ namespace _01_PublishSubscribe
}
- internal static IDependencyContainer GetUnityDependencyResolver()
- {
- var container = new UnityContainer();
-
- container.RegisterType();
- container.RegisterType();
-
- return new UnityDependencyResolver(container);
- }
-
-
internal static IDependencyContainer GetNinjectDependencyResolver()
{
var kernel = new StandardKernel();
diff --git a/Examples/02-DeclareDurableQueues/02-DeclareDurableQueues.csproj b/Examples/02-DeclareDurableQueues/02-DeclareDurableQueues.csproj
index 6049662..816a9c6 100644
--- a/Examples/02-DeclareDurableQueues/02-DeclareDurableQueues.csproj
+++ b/Examples/02-DeclareDurableQueues/02-DeclareDurableQueues.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_02_DeclareDurableQueues
+ enable
-
+
diff --git a/Examples/02-DeclareDurableQueues/Program.cs b/Examples/02-DeclareDurableQueues/Program.cs
index 935470a..c98234b 100644
--- a/Examples/02-DeclareDurableQueues/Program.cs
+++ b/Examples/02-DeclareDurableQueues/Program.cs
@@ -11,7 +11,7 @@ namespace _02_DeclareDurableQueues
{
public class Program
{
- public static void Main(string[] args)
+ public static void Main()
{
var container = new Container();
var dependencyResolver = new SimpleInjectorDependencyResolver(container);
@@ -30,19 +30,18 @@ namespace _02_DeclareDurableQueues
.EnableDeclareDurableQueues()
.Build();
- using (var connection = new TapetiConnection(config))
+ await using var connection = new TapetiConnection(config);
+
+ // This creates or updates the durable queue
+ await connection.Subscribe();
+
+ await dependencyResolver.Resolve().Publish(new PublishSubscribeMessage
{
- // This creates or updates the durable queue
- await connection.Subscribe();
+ Greeting = "Hello durable queue!"
+ });
- await dependencyResolver.Resolve().Publish(new PublishSubscribeMessage
- {
- Greeting = "Hello durable queue!"
- });
-
- // Wait for the controller to signal that the message has been received
- await waitForDone();
- }
+ // Wait for the controller to signal that the message has been received
+ await waitForDone();
}
}
}
diff --git a/Examples/03-FlowRequestResponse/03-FlowRequestResponse.csproj b/Examples/03-FlowRequestResponse/03-FlowRequestResponse.csproj
index 3af6a96..193404a 100644
--- a/Examples/03-FlowRequestResponse/03-FlowRequestResponse.csproj
+++ b/Examples/03-FlowRequestResponse/03-FlowRequestResponse.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_03_FlowRequestResponse
+ enable
-
+
diff --git a/Examples/03-FlowRequestResponse/ParallelFlowController.cs b/Examples/03-FlowRequestResponse/ParallelFlowController.cs
index 04d79f5..00f9687 100644
--- a/Examples/03-FlowRequestResponse/ParallelFlowController.cs
+++ b/Examples/03-FlowRequestResponse/ParallelFlowController.cs
@@ -15,9 +15,9 @@ namespace _03_FlowRequestResponse
private readonly IFlowProvider flowProvider;
private readonly IExampleState exampleState;
- public string FirstQuote;
- public string SecondQuote;
- public string ThirdQuote;
+ public string? FirstQuote;
+ public string? SecondQuote;
+ public string? ThirdQuote;
public ParallelFlowController(IFlowProvider flowProvider, IExampleState exampleState)
@@ -56,7 +56,7 @@ namespace _03_FlowRequestResponse
[Continuation]
- public async Task HandleSecondQuoteResponse(QuoteResponseMessage message, IFlowParallelRequest parallelRequest)
+ public async ValueTask HandleSecondQuoteResponse(QuoteResponseMessage message, IFlowParallelRequest parallelRequest)
{
Console.WriteLine("[ParallelFlowController] Second quote response received");
SecondQuote = message.Quote;
diff --git a/Examples/03-FlowRequestResponse/Program.cs b/Examples/03-FlowRequestResponse/Program.cs
index 50a361b..b83e86f 100644
--- a/Examples/03-FlowRequestResponse/Program.cs
+++ b/Examples/03-FlowRequestResponse/Program.cs
@@ -12,7 +12,7 @@ namespace _03_FlowRequestResponse
{
public class Program
{
- public static void Main(string[] args)
+ public static void Main()
{
var container = new Container();
var dependencyResolver = new SimpleInjectorDependencyResolver(container);
@@ -33,34 +33,33 @@ namespace _03_FlowRequestResponse
.Build();
- using (var connection = new TapetiConnection(config))
+ await using var connection = new TapetiConnection(config);
+
+ // Must be called before using any flow. When using a persistent repository like the
+ // SQL server implementation, you can run any required update scripts (for example, using DbUp)
+ // before calling this Load method.
+ // Call after creating the TapetiConnection, as it modifies the container to inject IPublisher.
+ await dependencyResolver.Resolve().Load();
+
+
+ await connection.Subscribe();
+
+
+ var flowStarter = dependencyResolver.Resolve();
+
+ var startData = new SimpleFlowController.StartData
{
- // Must be called before using any flow. When using a persistent repository like the
- // SQL server implementation, you can run any required update scripts (for example, using DbUp)
- // before calling this Load method.
- // Call after creating the TapetiConnection, as it modifies the container to inject IPublisher.
- await dependencyResolver.Resolve().Load();
+ RequestStartTime = DateTime.Now,
+ Amount = 1
+ };
- await connection.Subscribe();
+ await flowStarter.Start(c => c.StartFlow, startData);
+ await flowStarter.Start(c => c.StartFlow);
- var flowStarter = dependencyResolver.Resolve();
-
- var startData = new SimpleFlowController.StartData
- {
- RequestStartTime = DateTime.Now,
- Amount = 1
- };
-
-
- await flowStarter.Start(c => c.StartFlow, startData);
- await flowStarter.Start(c => c.StartFlow);
-
-
- // Wait for the controller to signal that the message has been received
- await waitForDone();
- }
+ // Wait for the controller to signal that the message has been received
+ await waitForDone();
}
}
}
diff --git a/Examples/03-FlowRequestResponse/ReceivingMessageController.cs b/Examples/03-FlowRequestResponse/ReceivingMessageController.cs
index 71d1ba6..97c498c 100644
--- a/Examples/03-FlowRequestResponse/ReceivingMessageController.cs
+++ b/Examples/03-FlowRequestResponse/ReceivingMessageController.cs
@@ -9,25 +9,16 @@ namespace _03_FlowRequestResponse
public class ReceivingMessageController
{
// No publisher required, responses can simply be returned
- public async Task HandleQuoteRequest(QuoteRequestMessage message)
+ public static async Task HandleQuoteRequest(QuoteRequestMessage message)
{
- string quote;
-
- switch (message.Amount)
+ var quote = message.Amount switch
{
- case 1:
+ 1 =>
// Well, they asked for it... :-)
- quote = "'";
- break;
-
- case 2:
- quote = "\"";
- break;
-
- default:
- quote = new string('\'', message.Amount);
- break;
- }
+ "'",
+ 2 => "\"",
+ _ => new string('\'', message.Amount)
+ };
// Just gonna let them wait for a bit, to demonstrate async message handlers
await Task.Delay(1000);
diff --git a/Examples/04-Transient/04-Transient.csproj b/Examples/04-Transient/04-Transient.csproj
index 7e1a085..5d94ffb 100644
--- a/Examples/04-Transient/04-Transient.csproj
+++ b/Examples/04-Transient/04-Transient.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_04_Transient
+ enable
-
+
diff --git a/Examples/04-Transient/Program.cs b/Examples/04-Transient/Program.cs
index 18b84f9..8e8e21a 100644
--- a/Examples/04-Transient/Program.cs
+++ b/Examples/04-Transient/Program.cs
@@ -13,7 +13,7 @@ namespace _04_Transient
{
public class Program
{
- public static void Main(string[] args)
+ public static void Main()
{
var container = new Container();
var dependencyResolver = new SimpleInjectorDependencyResolver(container);
@@ -34,22 +34,20 @@ namespace _04_Transient
.Build();
- using (var connection = new TapetiConnection(config))
- {
- await connection.Subscribe();
+ await using var connection = new TapetiConnection(config);
+ await connection.Subscribe();
- Console.WriteLine("Sending request...");
+ Console.WriteLine("Sending request...");
- var transientPublisher = dependencyResolver.Resolve();
- var response = await transientPublisher.RequestResponse(
- new LoggedInUsersRequestMessage());
+ var transientPublisher = dependencyResolver.Resolve();
+ var response = await transientPublisher.RequestResponse(
+ new LoggedInUsersRequestMessage());
- Console.WriteLine("Response: " + response.Count);
+ Console.WriteLine("Response: " + response.Count);
- // Unlike the other example, there is no need to call waitForDone, once we're here the response has been handled.
- }
+ // Unlike the other example, there is no need to call waitForDone, once we're here the response has been handled.
}
}
}
diff --git a/Examples/05-SpeedTest/05-SpeedTest.csproj b/Examples/05-SpeedTest/05-SpeedTest.csproj
index e51609f..a53241c 100644
--- a/Examples/05-SpeedTest/05-SpeedTest.csproj
+++ b/Examples/05-SpeedTest/05-SpeedTest.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_05_SpeedTest
+ enable
-
+
diff --git a/Examples/05-SpeedTest/Program.cs b/Examples/05-SpeedTest/Program.cs
index 6c399d7..11404af 100644
--- a/Examples/05-SpeedTest/Program.cs
+++ b/Examples/05-SpeedTest/Program.cs
@@ -21,7 +21,7 @@ namespace _05_SpeedTest
private const int ConcurrentTasks = 20;
- public static void Main(string[] args)
+ public static void Main()
{
var container = new Container();
var dependencyResolver = new SimpleInjectorDependencyResolver(container);
@@ -52,34 +52,32 @@ namespace _05_SpeedTest
.Build();
- using (var connection = new TapetiConnection(config))
- {
- var subscriber = await connection.Subscribe(false);
+ await using var connection = new TapetiConnection(config);
+ var subscriber = await connection.Subscribe(false);
- var publisher = dependencyResolver.Resolve();
- Console.WriteLine($"Publishing {MessageCount} messages...");
+ var publisher = dependencyResolver.Resolve();
+ Console.WriteLine($"Publishing {MessageCount} messages...");
- var stopwatch = new Stopwatch();
- stopwatch.Start();
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
- await PublishMessages(publisher);
+ await PublishMessages(publisher);
- stopwatch.Stop();
- Console.WriteLine($"Took {stopwatch.ElapsedMilliseconds} ms, {MessageCount / (stopwatch.ElapsedMilliseconds / 1000F):F0} messages/sec");
+ stopwatch.Stop();
+ Console.WriteLine($"Took {stopwatch.ElapsedMilliseconds} ms, {MessageCount / (stopwatch.ElapsedMilliseconds / 1000F):F0} messages/sec");
- Console.WriteLine("Consuming messages...");
- await subscriber.Resume();
+ Console.WriteLine("Consuming messages...");
+ await subscriber.Resume();
- stopwatch.Restart();
+ stopwatch.Restart();
- await waitForDone();
+ await waitForDone();
- stopwatch.Stop();
- Console.WriteLine($"Took {stopwatch.ElapsedMilliseconds} ms, {MessageCount / (stopwatch.ElapsedMilliseconds / 1000F):F0} messages/sec");
- }
+ stopwatch.Stop();
+ Console.WriteLine($"Took {stopwatch.ElapsedMilliseconds} ms, {MessageCount / (stopwatch.ElapsedMilliseconds / 1000F):F0} messages/sec");
}
diff --git a/Examples/05-SpeedTest/SpeedMessageController.cs b/Examples/05-SpeedTest/SpeedMessageController.cs
index 83af176..b0e5386 100644
--- a/Examples/05-SpeedTest/SpeedMessageController.cs
+++ b/Examples/05-SpeedTest/SpeedMessageController.cs
@@ -15,9 +15,11 @@ namespace _05_SpeedTest
}
+ #pragma warning disable IDE0060 // Remove unused parameter
public void HandleSpeedTestMessage(SpeedTestMessage message)
{
messageCounter.Add();
}
+ #pragma warning restore IDE0060
}
}
diff --git a/Examples/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj b/Examples/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj
index 347bb27..2ab68a4 100644
--- a/Examples/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj
+++ b/Examples/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_06_StatelessRequestResponse
+ enable
-
+
diff --git a/Examples/06-StatelessRequestResponse/Program.cs b/Examples/06-StatelessRequestResponse/Program.cs
index 6cc0de3..4e0e83e 100644
--- a/Examples/06-StatelessRequestResponse/Program.cs
+++ b/Examples/06-StatelessRequestResponse/Program.cs
@@ -12,7 +12,7 @@ namespace _06_StatelessRequestResponse
{
public class Program
{
- public static void Main(string[] args)
+ public static void Main()
{
var container = new Container();
var dependencyResolver = new SimpleInjectorDependencyResolver(container);
@@ -32,20 +32,18 @@ namespace _06_StatelessRequestResponse
.Build();
- using (var connection = new TapetiConnection(config))
- {
- await connection.Subscribe();
+ await using var connection = new TapetiConnection(config);
+ await connection.Subscribe();
- var publisher = dependencyResolver.Resolve();
- await publisher.PublishRequest(
- new QuoteRequestMessage
- {
- Amount = 1
- },
- c => c.HandleQuoteResponse);
+ var publisher = dependencyResolver.Resolve();
+ await publisher.PublishRequest(
+ new QuoteRequestMessage
+ {
+ Amount = 1
+ },
+ c => c.HandleQuoteResponse);
- await waitForDone();
- }
+ await waitForDone();
}
}
}
diff --git a/Examples/06-StatelessRequestResponse/ReceivingMessageController.cs b/Examples/06-StatelessRequestResponse/ReceivingMessageController.cs
index ea947c8..4a2704b 100644
--- a/Examples/06-StatelessRequestResponse/ReceivingMessageController.cs
+++ b/Examples/06-StatelessRequestResponse/ReceivingMessageController.cs
@@ -8,26 +8,16 @@ namespace _06_StatelessRequestResponse
public class ReceivingMessageController
{
// No publisher required, responses can simply be returned
- public QuoteResponseMessage HandleQuoteRequest(QuoteRequestMessage message)
+ public static QuoteResponseMessage HandleQuoteRequest(QuoteRequestMessage message)
{
- string quote;
-
- switch (message.Amount)
+ var quote = message.Amount switch
{
- case 1:
+ 1 =>
// Well, they asked for it... :-)
- quote = "'";
- break;
-
- case 2:
- quote = "\"";
- break;
-
- default:
- // We have to return a response.
- quote = null;
- break;
- }
+ "'",
+ 2 => "\"",
+ _ => null
+ };
return new QuoteResponseMessage
{
diff --git a/Examples/07-ParallelizationTest/07-ParallelizationTest.csproj b/Examples/07-ParallelizationTest/07-ParallelizationTest.csproj
index a51a70e..f799228 100644
--- a/Examples/07-ParallelizationTest/07-ParallelizationTest.csproj
+++ b/Examples/07-ParallelizationTest/07-ParallelizationTest.csproj
@@ -2,12 +2,13 @@
Exe
- net5.0
+ net6.0
_07_ParallelizationTest
+ enable
-
+
diff --git a/Examples/07-ParallelizationTest/ParallelizationMessageController.cs b/Examples/07-ParallelizationTest/ParallelizationMessageController.cs
index 66377a7..209ba39 100644
--- a/Examples/07-ParallelizationTest/ParallelizationMessageController.cs
+++ b/Examples/07-ParallelizationTest/ParallelizationMessageController.cs
@@ -16,9 +16,11 @@ namespace _07_ParallelizationTest
}
+ #pragma warning disable IDE0060 // Remove unused parameter
public async Task HandleSpeedTestMessage(SpeedTestMessage message)
{
await messageParallelization.WaitForBatch();
}
+ #pragma warning restore IDE0060
}
}
diff --git a/Examples/07-ParallelizationTest/Program.cs b/Examples/07-ParallelizationTest/Program.cs
index 99917d2..4011782 100644
--- a/Examples/07-ParallelizationTest/Program.cs
+++ b/Examples/07-ParallelizationTest/Program.cs
@@ -105,9 +105,9 @@ namespace _07_ParallelizationTest
private readonly Func done;
private readonly Action timeout;
private int count;
- private readonly object waitLock = new object();
- private TaskCompletionSource batchReachedTask = new TaskCompletionSource();
- private Timer messageExpectedTimer;
+ private readonly object waitLock = new();
+ private TaskCompletionSource batchReachedTask = new();
+ private Timer? messageExpectedTimer;
private readonly TimeSpan messageExpectedTimeout = TimeSpan.FromMilliseconds(5000);
@@ -124,7 +124,7 @@ namespace _07_ParallelizationTest
lock (waitLock)
{
if (messageExpectedTimer == null)
- messageExpectedTimer = new Timer(state =>
+ messageExpectedTimer = new Timer(_ =>
{
timeout(count);
}, null, messageExpectedTimeout, Timeout.InfiniteTimeSpan);
diff --git a/Examples/08-MessageHandlerLogging/08-MessageHandlerLogging.csproj b/Examples/08-MessageHandlerLogging/08-MessageHandlerLogging.csproj
index c249120..cc1f063 100644
--- a/Examples/08-MessageHandlerLogging/08-MessageHandlerLogging.csproj
+++ b/Examples/08-MessageHandlerLogging/08-MessageHandlerLogging.csproj
@@ -2,13 +2,14 @@
Exe
- net5.0
+ net6.0
_08_MessageHandlerLogging
+ enable
-
-
+
+
diff --git a/Examples/08-MessageHandlerLogging/Program.cs b/Examples/08-MessageHandlerLogging/Program.cs
index 2937dd9..3066d50 100644
--- a/Examples/08-MessageHandlerLogging/Program.cs
+++ b/Examples/08-MessageHandlerLogging/Program.cs
@@ -3,7 +3,6 @@ using System.Threading.Tasks;
using ExampleLib;
using Messaging.TapetiExample;
using Serilog;
-using Serilog.Events;
using SimpleInjector;
using Tapeti;
using Tapeti.Serilog;
diff --git a/Examples/ExampleLib/ExampleConsoleApp.cs b/Examples/ExampleLib/ExampleConsoleApp.cs
index cc248be..94aa865 100644
--- a/Examples/ExampleLib/ExampleConsoleApp.cs
+++ b/Examples/ExampleLib/ExampleConsoleApp.cs
@@ -24,7 +24,7 @@ namespace ExampleLib
private readonly IDependencyContainer dependencyResolver;
private readonly int expectedDoneCount;
private int doneCount;
- private readonly TaskCompletionSource doneSignal = new TaskCompletionSource();
+ private readonly TaskCompletionSource doneSignal = new();
/// Uses Tapeti's IDependencyContainer interface so you can easily switch an example to your favourite IoC container
@@ -79,7 +79,7 @@ namespace ExampleLib
{
while (true)
{
- if (!(e is AggregateException aggregateException))
+ if (e is not AggregateException aggregateException)
return e;
if (aggregateException.InnerExceptions.Count != 1)
diff --git a/Examples/ExampleLib/ExampleLib.csproj b/Examples/ExampleLib/ExampleLib.csproj
index d73eb4a..5d5c22e 100644
--- a/Examples/ExampleLib/ExampleLib.csproj
+++ b/Examples/ExampleLib/ExampleLib.csproj
@@ -1,8 +1,9 @@
- netstandard2.0
+ net6.0
true
+ enable
diff --git a/Examples/Messaging.TapetiExample/Messaging.TapetiExample.csproj b/Examples/Messaging.TapetiExample/Messaging.TapetiExample.csproj
index 13e2b2e..43a8b0c 100644
--- a/Examples/Messaging.TapetiExample/Messaging.TapetiExample.csproj
+++ b/Examples/Messaging.TapetiExample/Messaging.TapetiExample.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Tapeti.Autofac/AutofacDependencyResolver.cs b/Tapeti.Autofac/AutofacDependencyResolver.cs
index 74ac975..a2c48ae 100644
--- a/Tapeti.Autofac/AutofacDependencyResolver.cs
+++ b/Tapeti.Autofac/AutofacDependencyResolver.cs
@@ -1,10 +1,12 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Autofac;
using Autofac.Builder;
+// ReSharper disable UnusedMember.Global
+
namespace Tapeti.Autofac
{
- ///
///
/// Dependency resolver and container implementation for Autofac.
/// Since this class needs access to both the ContainerBuilder and the built IContainer,
@@ -13,22 +15,21 @@ namespace Tapeti.Autofac
///
public class AutofacDependencyResolver : IDependencyContainer
{
- private ContainerBuilder containerBuilder;
- private IContainer container;
+ private ContainerBuilder? containerBuilder;
+ private IContainer? container;
///
- /// The built container. Either set directly, or use the Build method to built the
+ /// The built container. Either set directly, or use the Build method to build the
/// update this reference.
///
public IContainer Container
{
- get => container;
+ get => container ?? throw new ArgumentNullException(nameof(container));
set
{
container = value;
- if (value != null)
- containerBuilder = null;
+ containerBuilder = null;
}
}
@@ -49,7 +50,7 @@ namespace Tapeti.Autofac
CheckContainerBuilder();
Container = containerBuilder.Build(options);
- return container;
+ return Container;
}
@@ -83,7 +84,7 @@ namespace Tapeti.Autofac
{
CheckContainerBuilder();
containerBuilder
- .Register(context => factory())
+ .Register(_ => factory())
.As()
.PreserveExistingDefaults();
}
@@ -116,7 +117,7 @@ namespace Tapeti.Autofac
{
CheckContainerBuilder();
containerBuilder
- .Register(context => factory())
+ .Register(_ => factory())
.As()
.SingleInstance()
.PreserveExistingDefaults();
@@ -140,6 +141,7 @@ namespace Tapeti.Autofac
}
+ [MemberNotNull(nameof(containerBuilder))]
private void CheckContainerBuilder()
{
if (containerBuilder == null)
diff --git a/Tapeti.Autofac/Tapeti.Autofac.csproj b/Tapeti.Autofac/Tapeti.Autofac.csproj
index b441b47..10e4b50 100644
--- a/Tapeti.Autofac/Tapeti.Autofac.csproj
+++ b/Tapeti.Autofac/Tapeti.Autofac.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Mark van Renswoude
@@ -11,10 +11,12 @@
https://github.com/MvRens/Tapeti
Tapeti.SimpleInjector.png
2.0.0
+ 9
+ enable
-
+
@@ -29,6 +31,6 @@
-
+
diff --git a/Tapeti.Benchmarks/Program.cs b/Tapeti.Benchmarks/Program.cs
new file mode 100644
index 0000000..3e45b81
--- /dev/null
+++ b/Tapeti.Benchmarks/Program.cs
@@ -0,0 +1,5 @@
+using BenchmarkDotNet.Running;
+using Tapeti.Benchmarks.Tests;
+
+BenchmarkRunner.Run();
+//new MethodInvokeBenchmarks().InvokeExpressionValueFactory();
\ No newline at end of file
diff --git a/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj b/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj
new file mode 100644
index 0000000..9aa7e19
--- /dev/null
+++ b/Tapeti.Benchmarks/Tapeti.Benchmarks.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs b/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs
new file mode 100644
index 0000000..601aa5b
--- /dev/null
+++ b/Tapeti.Benchmarks/Tests/MethodInvokeBenchmarks.cs
@@ -0,0 +1,112 @@
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using Tapeti.Helpers;
+
+#pragma warning disable CA1822 // Mark members as static - required for Benchmark.NET
+
+namespace Tapeti.Benchmarks.Tests
+{
+ [MemoryDiagnoser]
+ public class MethodInvokeBenchmarks
+ {
+ private delegate bool MethodToInvokeDelegate(object obj);
+
+ private static readonly MethodInfo MethodToInvokeInfo;
+ private static readonly MethodToInvokeDelegate MethodToInvokeDelegateInstance;
+ private static readonly ExpressionInvoke MethodToInvokeExpression;
+
+
+ static MethodInvokeBenchmarks()
+ {
+ MethodToInvokeInfo = typeof(MethodInvokeBenchmarks).GetMethod(nameof(MethodToInvoke))!;
+
+ var inputInstance = new MethodInvokeBenchmarks();
+ MethodToInvokeDelegateInstance = i => ((MethodInvokeBenchmarks)i).MethodToInvoke(inputInstance.GetSomeObject(), inputInstance.GetCancellationToken());
+ MethodToInvokeExpression = MethodToInvokeInfo.CreateExpressionInvoke();
+
+ /*
+
+ Fun experiment, but a bit too tricky for me at the moment.
+
+
+ var dynamicMethodToInvoke = new DynamicMethod(
+ nameof(MethodToInvoke),
+ typeof(bool),
+ new[] { typeof(object) },
+ typeof(MethodInvokeBenchmarks).Module);
+
+
+ var generator = dynamicMethodToInvoke.GetILGenerator(256);
+
+ generator.Emit(OpCodes.Ldarg_0); // Load the first argument (the instance) onto the stack
+ generator.Emit(OpCodes.Castclass, typeof(MethodInvokeBenchmarks)); // Cast to the expected instance type
+ generator.Emit(OpCodes.Ldc_I4_S, 42); // Push the first argument onto the stack
+ generator.EmitCall(OpCodes.Callvirt, MethodToInvokeInfo, null); // Call the method
+ generator.Emit(OpCodes.Ret);
+
+ MethodToInvokeEmitted = dynamicMethodToInvoke.CreateDelegate();
+ */
+ }
+
+
+ public bool MethodToInvoke(object someObject, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+
+ // ReSharper disable MemberCanBeMadeStatic.Local
+ private object GetSomeObject()
+ {
+ return new object();
+ }
+
+
+ private CancellationToken GetCancellationToken()
+ {
+ return CancellationToken.None;
+ }
+ // ReSharper restore MemberCanBeMadeStatic.Local
+
+
+
+ // For comparison
+ [Benchmark]
+ public bool Direct()
+ {
+ return MethodToInvoke(GetSomeObject(), GetCancellationToken());
+ }
+
+
+ // For comparison as well, as we don't know the signature beforehand
+ [Benchmark]
+ public bool Delegate()
+ {
+ var instance = new MethodInvokeBenchmarks();
+ return MethodToInvokeDelegateInstance(instance);
+ }
+
+
+ [Benchmark]
+ public bool MethodInvoke()
+ {
+ var instance = new MethodInvokeBenchmarks();
+ return (bool)(MethodToInvokeInfo.Invoke(instance, BindingFlags.DoNotWrapExceptions, null, new[] { GetSomeObject(), GetCancellationToken() }, null) ?? false);
+ }
+
+
+ [Benchmark]
+ public bool InvokeExpression()
+ {
+ var instance = new MethodInvokeBenchmarks();
+ return (bool)MethodToInvokeExpression(instance, GetSomeObject(), GetCancellationToken());
+ }
+
+ //[Benchmark]
+ //public bool ReflectionEmit()
+ //{
+ // var instance = new MethodInvokeBenchmarks();
+ // return MethodToInvokeEmitted(instance);
+ //}
+ }
+}
diff --git a/Tapeti.CastleWindsor/Tapeti.CastleWindsor.csproj b/Tapeti.CastleWindsor/Tapeti.CastleWindsor.csproj
index d8546d8..bd1af38 100644
--- a/Tapeti.CastleWindsor/Tapeti.CastleWindsor.csproj
+++ b/Tapeti.CastleWindsor/Tapeti.CastleWindsor.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Mark van Renswoude
@@ -11,10 +11,12 @@
https://github.com/MvRens/Tapeti
Tapeti.SimpleInjector.png
2.0.0
+ 9
+ enable
-
+
@@ -29,6 +31,6 @@
-
+
diff --git a/Tapeti.CastleWindsor/WindsorDependencyResolver.cs b/Tapeti.CastleWindsor/WindsorDependencyResolver.cs
index e398157..95d5f5e 100644
--- a/Tapeti.CastleWindsor/WindsorDependencyResolver.cs
+++ b/Tapeti.CastleWindsor/WindsorDependencyResolver.cs
@@ -4,7 +4,6 @@ using Castle.Windsor;
namespace Tapeti.CastleWindsor
{
- ///
///
/// Dependency resolver and container implementation for Castle Windsor.
///
diff --git a/Tapeti.DataAnnotations/DataAnnotationsExtension.cs b/Tapeti.DataAnnotations/DataAnnotationsExtension.cs
index abdbc5c..11ac2b3 100644
--- a/Tapeti.DataAnnotations/DataAnnotationsExtension.cs
+++ b/Tapeti.DataAnnotations/DataAnnotationsExtension.cs
@@ -3,7 +3,6 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
- ///
///
/// Provides the DataAnnotations validation middleware.
///
diff --git a/Tapeti.DataAnnotations/DataAnnotationsMessageMiddleware.cs b/Tapeti.DataAnnotations/DataAnnotationsMessageMiddleware.cs
index 8b2ed85..63dbd0d 100644
--- a/Tapeti.DataAnnotations/DataAnnotationsMessageMiddleware.cs
+++ b/Tapeti.DataAnnotations/DataAnnotationsMessageMiddleware.cs
@@ -5,19 +5,21 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
- ///
///
/// Validates consumed messages using System.ComponentModel.DataAnnotations
///
internal class DataAnnotationsMessageMiddleware : IMessageMiddleware
{
///
- public async Task Handle(IMessageContext context, Func next)
+ public ValueTask Handle(IMessageContext context, Func next)
{
+ if (context.Message == null)
+ return next();
+
var validationContext = new ValidationContext(context.Message);
Validator.ValidateObject(context.Message, validationContext, true);
- await next();
+ return next();
}
}
}
diff --git a/Tapeti.DataAnnotations/DataAnnotationsPublishMiddleware.cs b/Tapeti.DataAnnotations/DataAnnotationsPublishMiddleware.cs
index 514989c..025d056 100644
--- a/Tapeti.DataAnnotations/DataAnnotationsPublishMiddleware.cs
+++ b/Tapeti.DataAnnotations/DataAnnotationsPublishMiddleware.cs
@@ -5,19 +5,18 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
- ///
///
/// Validates published messages using System.ComponentModel.DataAnnotations
///
internal class DataAnnotationsPublishMiddleware : IPublishMiddleware
{
///
- public async Task Handle(IPublishContext context, Func next)
+ public ValueTask Handle(IPublishContext context, Func next)
{
var validationContext = new ValidationContext(context.Message);
Validator.ValidateObject(context.Message, validationContext, true);
- await next();
+ return next();
}
}
}
diff --git a/Tapeti.DataAnnotations/Tapeti.DataAnnotations.csproj b/Tapeti.DataAnnotations/Tapeti.DataAnnotations.csproj
index 17f465c..713493f 100644
--- a/Tapeti.DataAnnotations/Tapeti.DataAnnotations.csproj
+++ b/Tapeti.DataAnnotations/Tapeti.DataAnnotations.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Mark van Renswoude
@@ -11,6 +11,8 @@
https://github.com/MvRens/Tapeti
Tapeti.DataAnnotations.png
2.0.0
+ 9
+ enable
@@ -33,6 +35,6 @@
-
+
diff --git a/Tapeti.Flow.SQL/ConfigExtensions.cs b/Tapeti.Flow.SQL/ConfigExtensions.cs
index ef285d9..a70cc22 100644
--- a/Tapeti.Flow.SQL/ConfigExtensions.cs
+++ b/Tapeti.Flow.SQL/ConfigExtensions.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Linq;
using Tapeti.Config;
// ReSharper disable UnusedMember.Global
@@ -45,7 +46,7 @@ namespace Tapeti.Flow.SQL
public IEnumerable
-
+
@@ -42,6 +49,6 @@
-
+
diff --git a/Tapeti.Flow/Annotations/ContinuationAttribute.cs b/Tapeti.Flow/Annotations/ContinuationAttribute.cs
index 2612a30..e1fa2c3 100644
--- a/Tapeti.Flow/Annotations/ContinuationAttribute.cs
+++ b/Tapeti.Flow/Annotations/ContinuationAttribute.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Flow.Annotations
{
- ///
///
/// Marks a message handler as a response message handler which continues a Tapeti Flow.
/// The method only receives direct messages, and does not create a routing key based binding to the queue.
diff --git a/Tapeti.Flow/Annotations/StartAttribute.cs b/Tapeti.Flow/Annotations/StartAttribute.cs
index 8c1fd2e..d43c274 100644
--- a/Tapeti.Flow/Annotations/StartAttribute.cs
+++ b/Tapeti.Flow/Annotations/StartAttribute.cs
@@ -3,7 +3,6 @@ using JetBrains.Annotations;
namespace Tapeti.Flow.Annotations
{
- ///
///
/// Marks this method as the start of a Tapeti Flow. Use IFlowStarter.Start to begin a new flow and
/// call this method. Must return an IYieldPoint.
diff --git a/Tapeti.Flow/ConfigExtensions.cs b/Tapeti.Flow/ConfigExtensions.cs
index 474da6a..cfc72a4 100644
--- a/Tapeti.Flow/ConfigExtensions.cs
+++ b/Tapeti.Flow/ConfigExtensions.cs
@@ -1,5 +1,7 @@
using Tapeti.Config;
+// ReSharper disable UnusedMember.Global
+
namespace Tapeti.Flow
{
///
@@ -13,7 +15,7 @@ namespace Tapeti.Flow
///
/// An optional IFlowRepository implementation to persist flow state. If not provided, flow state will be lost when the application restarts.
///
- public static ITapetiConfigBuilder WithFlow(this ITapetiConfigBuilder config, IFlowRepository flowRepository = null)
+ public static ITapetiConfigBuilder WithFlow(this ITapetiConfigBuilder config, IFlowRepository? flowRepository = null)
{
config.Use(new FlowExtension(flowRepository));
return config;
diff --git a/Tapeti.Flow/Default/FlowBindingMiddleware.cs b/Tapeti.Flow/Default/FlowBindingMiddleware.cs
index c0f9565..c5db483 100644
--- a/Tapeti.Flow/Default/FlowBindingMiddleware.cs
+++ b/Tapeti.Flow/Default/FlowBindingMiddleware.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -9,6 +10,17 @@ using Tapeti.Helpers;
namespace Tapeti.Flow.Default
{
+ #if !NET7_0_OR_GREATER
+ #pragma warning disable CS1591
+ public class UnreachableException : Exception
+ {
+ public UnreachableException(string message) : base(message)
+ {
+ }
+ }
+ #pragma warning restore CS1591
+ #endif
+
internal class FlowBindingMiddleware : IControllerBindingMiddleware
{
public void Handle(IControllerBindingContext context, Action next)
@@ -31,6 +43,9 @@ namespace Tapeti.Flow.Default
if (continuationAttribute == null)
return;
+ if (context.Method.IsStatic)
+ throw new ArgumentException($"Continuation attribute is not valid on static methods in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}");
+
context.SetBindingTargetMode(BindingTargetMode.Direct);
context.Use(new FlowContinuationMiddleware());
@@ -43,16 +58,31 @@ namespace Tapeti.Flow.Default
{
context.Result.SetHandler(async (messageContext, value) =>
{
+ if (value == null)
+ throw new InvalidOperationException("Return value should be a Task, not null");
+
await (Task)value;
await HandleParallelResponse(messageContext);
});
}
+ if (context.Result.Info.ParameterType == typeof(ValueTask))
+ {
+ context.Result.SetHandler(async (messageContext, value) =>
+ {
+ if (value == null)
+ // ValueTask is a struct and should never be null
+ throw new UnreachableException("Return value should be a ValueTask, not null");
+
+ await (ValueTask)value;
+ await HandleParallelResponse(messageContext);
+ });
+ }
else if (context.Result.Info.ParameterType == typeof(void))
{
- context.Result.SetHandler((messageContext, value) => HandleParallelResponse(messageContext));
+ context.Result.SetHandler((messageContext, _) => HandleParallelResponse(messageContext));
}
else
- throw new ArgumentException($"Result type must be IYieldPoint, Task or void in controller {context. Method.DeclaringType?.FullName}, method {context.Method.Name}");
+ throw new ArgumentException($"Result type must be IYieldPoint, Task or void in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}");
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType == typeof(IFlowParallelRequest)))
@@ -62,34 +92,64 @@ namespace Tapeti.Flow.Default
private static void RegisterYieldPointResult(IControllerBindingContext context)
{
- if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out var isTaskOf))
+ if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out var taskType))
return;
- if (isTaskOf)
+ if (context.Method.IsStatic)
+ throw new ArgumentException($"Yield points are not valid on static methods in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}");
+
+ switch (taskType)
{
- context.Result.SetHandler(async (messageContext, value) =>
- {
- var yieldPoint = await (Task)value;
- if (yieldPoint != null)
+ case TaskType.None:
+ context.Result.SetHandler((messageContext, value) =>
+ {
+ if (value == null)
+ throw new InvalidOperationException("Return value should be an IYieldPoint, not null");
+
+ return HandleYieldPoint(messageContext, (IYieldPoint)value);
+ });
+ break;
+
+ case TaskType.Task:
+ context.Result.SetHandler(async (messageContext, value) =>
+ {
+ if (value == null)
+ throw new InvalidOperationException("Return value should be a Task, not null");
+
+ var yieldPoint = await (Task)value;
await HandleYieldPoint(messageContext, yieldPoint);
- });
+ });
+ break;
+
+ case TaskType.ValueTask:
+ context.Result.SetHandler(async (messageContext, value) =>
+ {
+ if (value == null)
+ // ValueTask is a struct and should never be null
+ throw new UnreachableException("Return value should be a ValueTask, not null");
+
+ var yieldPoint = await (ValueTask)value;
+ await HandleYieldPoint(messageContext, yieldPoint);
+ });
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- else
- context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
}
- private static Task HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
+ private static ValueTask HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
{
var flowHandler = context.Config.DependencyResolver.Resolve();
return flowHandler.Execute(new FlowHandlerContext(context), yieldPoint);
}
- private static Task HandleParallelResponse(IMessageContext context)
+ private static ValueTask HandleParallelResponse(IMessageContext context)
{
if (context.TryGet(out var flowPayload) && flowPayload.FlowIsConverging)
- return Task.CompletedTask;
+ return default;
var flowHandler = context.Config.DependencyResolver.Resolve();
return flowHandler.Execute(new FlowHandlerContext(context), new DelegateYieldPoint(async flowContext =>
@@ -110,7 +170,7 @@ namespace Tapeti.Flow.Default
}
- private static object ParallelRequestParameterFactory(IMessageContext context)
+ private static object? ParallelRequestParameterFactory(IMessageContext context)
{
var flowHandler = context.Config.DependencyResolver.Resolve();
return flowHandler.GetParallelRequest(new FlowHandlerContext(context));
diff --git a/Tapeti.Flow/Default/FlowContext.cs b/Tapeti.Flow/Default/FlowContext.cs
index 70f98f9..1c23143 100644
--- a/Tapeti.Flow/Default/FlowContext.cs
+++ b/Tapeti.Flow/Default/FlowContext.cs
@@ -6,35 +6,57 @@ namespace Tapeti.Flow.Default
{
internal class FlowContext : IDisposable
{
- public IFlowHandlerContext HandlerContext { get; set; }
- public IFlowStateLock FlowStateLock { get; set; }
- public FlowState FlowState { get; set; }
+ private readonly IFlowHandlerContext? handlerContext;
+ private IFlowStateLock? flowStateLock;
+ private FlowState? flowState;
+
+
+ public IFlowHandlerContext HandlerContext => handlerContext ?? throw new InvalidOperationException("FlowContext does not have a HandlerContext");
+ public IFlowStateLock FlowStateLock => flowStateLock ?? throw new InvalidOperationException("FlowContext does not have a FlowStateLock");
+ public FlowState FlowState => flowState ?? throw new InvalidOperationException("FlowContext does not have a FlowState");
+
+ public bool HasFlowStateAndLock => flowState != null && flowStateLock != null;
public Guid ContinuationID { get; set; }
- public ContinuationMetadata ContinuationMetadata { get; set; }
+ public ContinuationMetadata? ContinuationMetadata { get; set; }
private int storeCalled;
private int deleteCalled;
- public async Task Store(bool persistent)
+ public FlowContext(IFlowHandlerContext handlerContext, FlowState flowState, IFlowStateLock flowStateLock)
+ {
+ this.flowState = flowState;
+ this.flowStateLock = flowStateLock;
+ this.handlerContext = handlerContext;
+ }
+
+
+ public FlowContext(IFlowHandlerContext handlerContext)
+ {
+ this.handlerContext = handlerContext;
+ }
+
+
+ public void SetFlowState(FlowState newFlowState, IFlowStateLock newFlowStateLock)
+ {
+ flowState = newFlowState;
+ flowStateLock = newFlowStateLock;
+ }
+
+
+ public ValueTask Store(bool persistent)
{
storeCalled++;
- if (HandlerContext == null) throw new ArgumentNullException(nameof(HandlerContext));
- if (FlowState == null) throw new ArgumentNullException(nameof(FlowState));
- if (FlowStateLock == null) throw new ArgumentNullException(nameof(FlowStateLock));
-
FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(HandlerContext.Controller);
- await FlowStateLock.StoreFlowState(FlowState, persistent);
+ return FlowStateLock.StoreFlowState(FlowState, persistent);
}
- public async Task Delete()
+ public ValueTask Delete()
{
deleteCalled++;
-
- if (FlowStateLock != null)
- await FlowStateLock.DeleteFlowState();
+ return flowStateLock?.DeleteFlowState() ?? default;
}
public bool IsStoredOrDeleted()
@@ -45,7 +67,7 @@ namespace Tapeti.Flow.Default
public void EnsureStoreOrDeleteIsCalled()
{
if (!IsStoredOrDeleted())
- throw new InvalidProgramException("Neither Store nor Delete are called for the state of the current flow. FlowID = " + FlowStateLock?.FlowID);
+ throw new InvalidProgramException("Neither Store nor Delete are called for the state of the current flow. FlowID = " + flowStateLock?.FlowID);
Debug.Assert(storeCalled <= 1, "Store called more than once!");
Debug.Assert(deleteCalled <= 1, "Delete called more than once!");
@@ -53,7 +75,7 @@ namespace Tapeti.Flow.Default
public void Dispose()
{
- FlowStateLock?.Dispose();
+ flowStateLock?.Dispose();
}
}
}
diff --git a/Tapeti.Flow/Default/FlowContinuationMiddleware.cs b/Tapeti.Flow/Default/FlowContinuationMiddleware.cs
index e516667..51098d3 100644
--- a/Tapeti.Flow/Default/FlowContinuationMiddleware.cs
+++ b/Tapeti.Flow/Default/FlowContinuationMiddleware.cs
@@ -1,5 +1,4 @@
using System;
-using System.Reflection;
using System.Threading.Tasks;
using Tapeti.Config;
using Tapeti.Flow.FlowHelpers;
@@ -12,7 +11,7 @@ namespace Tapeti.Flow.Default
///
internal class FlowContinuationMiddleware : IControllerFilterMiddleware, IControllerMessageMiddleware, IControllerCleanupMiddleware
{
- public async Task Filter(IMessageContext context, Func next)
+ public async ValueTask Filter(IMessageContext context, Func next)
{
if (!context.TryGet(out var controllerPayload))
return;
@@ -28,15 +27,19 @@ namespace Tapeti.Flow.Default
}
- public async Task Handle(IMessageContext context, Func next)
+ public async ValueTask Handle(IMessageContext context, Func next)
{
if (!context.TryGet(out var controllerPayload))
return;
if (context.TryGet(out var flowPayload))
{
+ if (controllerPayload.Controller == null)
+ throw new InvalidOperationException("Controller is not available (method is static?)");
+
var flowContext = flowPayload.FlowContext;
- Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, controllerPayload.Controller);
+ if (!string.IsNullOrEmpty(flowContext.FlowState.Data))
+ Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, controllerPayload.Controller);
// Remove Continuation now because the IYieldPoint result handler will store the new state
flowContext.FlowState.Continuations.Remove(flowContext.ContinuationID);
@@ -54,7 +57,7 @@ namespace Tapeti.Flow.Default
}
- public async Task Cleanup(IMessageContext context, ConsumeResult consumeResult, Func next)
+ public async ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult, Func next)
{
await next();
@@ -66,11 +69,11 @@ namespace Tapeti.Flow.Default
var flowContext = flowPayload.FlowContext;
- if (flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(controllerPayload.Binding.Method))
+ if (flowContext.ContinuationMetadata == null || flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(controllerPayload.Binding.Method))
// Do not call when the controller method was filtered, if the same message has two methods
return;
- if (flowContext.FlowStateLock != null)
+ if (flowContext.HasFlowStateAndLock)
{
if (!flowContext.IsStoredOrDeleted())
// The exception strategy can set the consume result to Success. Instead, check if the yield point
@@ -83,7 +86,7 @@ namespace Tapeti.Flow.Default
- private static async Task EnrichWithFlowContext(IMessageContext context)
+ private static async ValueTask EnrichWithFlowContext(IMessageContext context)
{
if (context.TryGet(out var flowPayload))
return flowPayload.FlowContext;
@@ -107,13 +110,8 @@ namespace Tapeti.Flow.Default
if (flowState == null)
return null;
- var flowContext = new FlowContext
+ var flowContext = new FlowContext(new FlowHandlerContext(context), flowState, flowStateLock)
{
- HandlerContext = new FlowHandlerContext(context),
-
- FlowStateLock = flowStateLock,
- FlowState = flowState,
-
ContinuationID = continuationID,
ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out var continuation) ? continuation : null
};
diff --git a/Tapeti.Flow/Default/FlowHandlerContext.cs b/Tapeti.Flow/Default/FlowHandlerContext.cs
index c1e8c38..da816c6 100644
--- a/Tapeti.Flow/Default/FlowHandlerContext.cs
+++ b/Tapeti.Flow/Default/FlowHandlerContext.cs
@@ -3,7 +3,6 @@ using Tapeti.Config;
namespace Tapeti.Flow.Default
{
- ///
///
/// Default implementation for IFlowHandlerContext
///
@@ -11,8 +10,11 @@ namespace Tapeti.Flow.Default
{
///
///
- public FlowHandlerContext()
+ public FlowHandlerContext(ITapetiConfig config, object? controller, MethodInfo method)
{
+ Config = config;
+ Controller = controller;
+ Method = method;
}
@@ -20,11 +22,7 @@ namespace Tapeti.Flow.Default
///
public FlowHandlerContext(IMessageContext source)
{
- if (source == null)
- return;
-
- if (!source.TryGet(out var controllerPayload))
- return;
+ var controllerPayload = source.Get();
Config = source.Config;
Controller = controllerPayload.Controller;
@@ -39,15 +37,15 @@ namespace Tapeti.Flow.Default
}
///
- public ITapetiConfig Config { get; set; }
+ public ITapetiConfig Config { get; }
///
- public object Controller { get; set; }
+ public object? Controller { get; }
///
- public MethodInfo Method { get; set; }
+ public MethodInfo Method { get; }
///
- public IMessageContext MessageContext { get; set; }
+ public IMessageContext? MessageContext { get; }
}
}
diff --git a/Tapeti.Flow/Default/FlowProvider.cs b/Tapeti.Flow/Default/FlowProvider.cs
index 8b6d416..0ef8aa3 100644
--- a/Tapeti.Flow/Default/FlowProvider.cs
+++ b/Tapeti.Flow/Default/FlowProvider.cs
@@ -32,14 +32,21 @@ namespace Tapeti.Flow.Default
///
- public IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler)
+ public IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler) where TRequest : class where TResponse : class
{
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
}
///
- public IYieldPoint YieldWithRequestSync(TRequest message, Func responseHandler)
+ public IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler) where TRequest : class where TResponse : class
+ {
+ var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
+ return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
+ }
+
+ ///
+ public IYieldPoint YieldWithRequestSync(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
{
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
@@ -52,7 +59,7 @@ namespace Tapeti.Flow.Default
}
///
- public IYieldPoint EndWithResponse(TResponse message)
+ public IYieldPoint EndWithResponse(TResponse message) where TResponse : class
{
return new DelegateYieldPoint(context => SendResponse(context, message));
}
@@ -65,9 +72,9 @@ namespace Tapeti.Flow.Default
internal async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo,
- string convergeMethodName = null, bool convergeMethodTaskSync = false, bool store = true)
+ string? convergeMethodName = null, bool convergeMethodTaskSync = false, bool store = true)
{
- if (context.FlowState == null)
+ if (!context.HasFlowStateAndLock)
{
await CreateNewFlowState(context);
Debug.Assert(context.FlowState != null, "context.FlowState != null");
@@ -98,9 +105,9 @@ namespace Tapeti.Flow.Default
private async Task SendResponse(FlowContext context, object message)
{
- var reply = context.FlowState == null
- ? GetReply(context.HandlerContext)
- : context.FlowState.Metadata.Reply;
+ var reply = context.HasFlowStateAndLock
+ ? context.FlowState.Metadata.Reply
+ : GetReply(context.HandlerContext);
if (reply == null)
throw new YieldPointException("No response is required");
@@ -127,7 +134,7 @@ namespace Tapeti.Flow.Default
{
await context.Delete();
- if (context.FlowState?.Metadata.Reply != null)
+ if (context.HasFlowStateAndLock && context.FlowState.Metadata.Reply != null)
throw new YieldPointException($"Flow must end with a response message of type {context.FlowState.Metadata.Reply.ResponseTypeName}");
}
@@ -152,16 +159,15 @@ namespace Tapeti.Flow.Default
if (binding.QueueName == null)
throw new ArgumentException("responseHandler is not yet subscribed to a queue, TapetiConnection.Subscribe must be called before starting a flow", nameof(responseHandler));
- return new ResponseHandlerInfo
- {
- MethodName = MethodSerializer.Serialize(responseHandler.Method),
- ReplyToQueue = binding.QueueName,
- IsDurableQueue = binding.QueueType == QueueType.Durable
- };
+ return new ResponseHandlerInfo(
+ MethodSerializer.Serialize(responseHandler.Method),
+ binding.QueueName,
+ binding.QueueType == QueueType.Durable
+ );
}
- private static ReplyMetadata GetReply(IFlowHandlerContext context)
+ private static ReplyMetadata? GetReply(IFlowHandlerContext context)
{
var requestAttribute = context.MessageContext?.Message?.GetType().GetCustomAttribute();
if (requestAttribute?.Response == null)
@@ -169,7 +175,7 @@ namespace Tapeti.Flow.Default
return new ReplyMetadata
{
- CorrelationId = context.MessageContext.Properties.CorrelationId,
+ CorrelationId = context.MessageContext!.Properties.CorrelationId,
ReplyTo = context.MessageContext.Properties.ReplyTo,
ResponseTypeName = requestAttribute.Response.FullName,
Mandatory = context.MessageContext.Properties.Persistent.GetValueOrDefault(true)
@@ -181,39 +187,34 @@ namespace Tapeti.Flow.Default
var flowStore = flowContext.HandlerContext.Config.DependencyResolver.Resolve();
var flowID = Guid.NewGuid();
- flowContext.FlowStateLock = await flowStore.LockFlowState(flowID);
+ var flowStateLock = await flowStore.LockFlowState(flowID);
- if (flowContext.FlowStateLock == null)
+ if (flowStateLock == null)
throw new InvalidOperationException("Unable to lock a new flow");
- flowContext.FlowState = new FlowState
+ var flowState = new FlowState
{
- Metadata = new FlowMetadata
- {
- Reply = GetReply(flowContext.HandlerContext)
- }
+ Metadata = new FlowMetadata(GetReply(flowContext.HandlerContext))
};
+
+ flowContext.SetFlowState(flowState, flowStateLock);
}
///
- public async Task Execute(IFlowHandlerContext context, IYieldPoint yieldPoint)
+ public async ValueTask Execute(IFlowHandlerContext context, IYieldPoint yieldPoint)
{
- if (!(yieldPoint is DelegateYieldPoint executableYieldPoint))
- throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Method.Name}");
+ if (yieldPoint is not DelegateYieldPoint executableYieldPoint)
+ throw new YieldPointException($"Yield point is required in controller {context.Controller?.GetType().Name} for method {context.Method.Name}");
- FlowContext flowContext = null;
+ FlowContext? flowContext = null;
var disposeFlowContext = false;
try
{
- var messageContext = context.MessageContext;
- if (messageContext == null || !messageContext.TryGet(out var flowPayload))
+ if (context.MessageContext == null || !context.MessageContext.TryGet(out var flowPayload))
{
- flowContext = new FlowContext
- {
- HandlerContext = context
- };
+ flowContext = new FlowContext(context);
// If we ended up here it is because of a Start. No point in storing the new FlowContext
// in the messageContext as the yield point is the last to execute.
@@ -229,7 +230,7 @@ namespace Tapeti.Flow.Default
catch (YieldPointException e)
{
// Useful for debugging
- e.Data["Tapeti.Controller.Name"] = context.Controller.GetType().FullName;
+ e.Data["Tapeti.Controller.Name"] = context.Controller?.GetType().FullName;
e.Data["Tapeti.Controller.Method"] = context.Method.Name;
throw;
}
@@ -239,44 +240,66 @@ namespace Tapeti.Flow.Default
finally
{
if (disposeFlowContext)
- flowContext.Dispose();
+ flowContext?.Dispose();
}
}
///
- public IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context)
+ public IFlowParallelRequest? GetParallelRequest(IFlowHandlerContext context)
{
- return context.MessageContext.TryGet(out var flowPayload)
+ return context.MessageContext != null && context.MessageContext.TryGet(out var flowPayload)
? new ParallelRequest(config, this, flowPayload.FlowContext)
: null;
}
///
- public Task Converge(IFlowHandlerContext context)
+ public ValueTask Converge(IFlowHandlerContext context)
{
- return Execute(context, new DelegateYieldPoint(flowContext =>
- Converge(flowContext, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync)));
+ return Execute(context, new DelegateYieldPoint(async flowContext =>
+ {
+ if (flowContext.ContinuationMetadata == null)
+ throw new InvalidOperationException("Missing ContinuationMetadata in FlowContext");
+
+ if (flowContext.ContinuationMetadata.ConvergeMethodName == null)
+ throw new InvalidOperationException("Missing ConvergeMethodName in FlowContext ContinuationMetadata");
+
+ await Converge(flowContext, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync);
+ }));
}
internal async Task Converge(FlowContext flowContext, string convergeMethodName, bool convergeMethodSync)
{
- IYieldPoint yieldPoint;
+ IYieldPoint? yieldPoint;
+
+ if (flowContext.HandlerContext == null)
+ throw new InvalidOperationException($"Missing HandleContext in FlowContext for converge method {convergeMethodName}");
+
+ if (flowContext.HandlerContext.MessageContext == null)
+ throw new InvalidOperationException($"Missing MessageContext in FlowContext for converge method {convergeMethodName}");
if (!flowContext.HandlerContext.MessageContext.TryGet(out var controllerPayload))
throw new ArgumentException("Context does not contain a controller payload", nameof(flowContext));
+ if (controllerPayload.Controller == null)
+ throw new InvalidOperationException($"Controller is not available for converge method {convergeMethodName} (method is static?)");
var method = controllerPayload.Controller.GetType().GetMethod(convergeMethodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
throw new ArgumentException($"Unknown converge method in controller {controllerPayload.Controller.GetType().Name}: {convergeMethodName}");
if (convergeMethodSync)
- yieldPoint = (IYieldPoint)method.Invoke(controllerPayload.Controller, new object[] { });
+ yieldPoint = (IYieldPoint?)method.Invoke(controllerPayload.Controller, new object[] { });
else
- yieldPoint = await(Task)method.Invoke(controllerPayload.Controller, new object[] { });
+ {
+ var yieldPointTask = method.Invoke(controllerPayload.Controller, new object[] { });
+ if (yieldPointTask == null)
+ throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}");
+
+ yieldPoint = await (Task)yieldPointTask;
+ }
if (yieldPoint == null)
throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}");
@@ -290,14 +313,21 @@ namespace Tapeti.Flow.Default
{
private class RequestInfo
{
- public object Message { get; set; }
- public ResponseHandlerInfo ResponseHandlerInfo { get; set; }
+ public object Message { get; }
+ public ResponseHandlerInfo ResponseHandlerInfo { get; }
+
+
+ public RequestInfo(object message, ResponseHandlerInfo responseHandlerInfo)
+ {
+ Message = message;
+ ResponseHandlerInfo = responseHandlerInfo;
+ }
}
private readonly ITapetiConfig config;
private readonly FlowProvider flowProvider;
- private readonly List requests = new List();
+ private readonly List requests = new();
public ParallelRequestBuilder(ITapetiConfig config, FlowProvider flowProvider)
@@ -307,32 +337,40 @@ namespace Tapeti.Flow.Default
}
- public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler)
+ public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
+ {
+ return InternalAddRequest(message, responseHandler);
+ }
+
+ public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
+ {
+ return InternalAddRequest(message, responseHandler);
+ }
+
+ public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
+ {
+ return InternalAddRequest(message, responseHandler);
+ }
+
+ public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
+ {
+ return InternalAddRequest(message, responseHandler);
+ }
+
+ public IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class
+ {
+ return InternalAddRequest(message, responseHandler);
+ }
+
+ public IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class
{
return InternalAddRequest(message, responseHandler);
}
- public IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler)
+ private IFlowParallelRequestBuilder InternalAddRequest(object message, Delegate responseHandler)
{
- return InternalAddRequest(message, responseHandler);
- }
-
-
- public IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler)
- {
- return InternalAddRequest(message, responseHandler);
- }
-
-
- public IFlowParallelRequestBuilder InternalAddRequest(object message, Delegate responseHandler)
- {
- requests.Add(new RequestInfo
- {
- Message = message,
- ResponseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler)
- });
-
+ requests.Add(new RequestInfo(message, GetResponseHandlerInfo(config, message, responseHandler)));
return this;
}
@@ -342,7 +380,6 @@ namespace Tapeti.Flow.Default
return BuildYieldPoint(continuation, false, noRequestsBehaviour);
}
-
public IYieldPoint YieldSync(Func continuation, FlowNoRequestsBehaviour noRequestsBehaviour = FlowNoRequestsBehaviour.Exception)
{
return BuildYieldPoint(continuation, true, noRequestsBehaviour);
@@ -353,29 +390,21 @@ namespace Tapeti.Flow.Default
{
if (requests.Count == 0)
{
- switch (noRequestsBehaviour)
+ return noRequestsBehaviour switch
{
- case FlowNoRequestsBehaviour.Exception:
- throw new YieldPointException("At least one request must be added before yielding a parallel request");
-
- case FlowNoRequestsBehaviour.Converge:
- return new DelegateYieldPoint(context =>
- flowProvider.Converge(context, convergeMethod.Method.Name, convergeMethodSync));
-
- case FlowNoRequestsBehaviour.EndFlow:
- return new DelegateYieldPoint(EndFlow);
-
- default:
- throw new ArgumentOutOfRangeException(nameof(noRequestsBehaviour), noRequestsBehaviour, null);
- }
+ FlowNoRequestsBehaviour.Exception => throw new YieldPointException("At least one request must be added before yielding a parallel request"),
+ FlowNoRequestsBehaviour.Converge => new DelegateYieldPoint(context => flowProvider.Converge(context, convergeMethod.Method.Name, convergeMethodSync)),
+ FlowNoRequestsBehaviour.EndFlow => new DelegateYieldPoint(EndFlow),
+ _ => throw new ArgumentOutOfRangeException(nameof(noRequestsBehaviour), noRequestsBehaviour, null)
+ };
}
- if (convergeMethod?.Method == null)
+ if (convergeMethod.Method == null)
throw new ArgumentNullException(nameof(convergeMethod));
return new DelegateYieldPoint(async context =>
{
- if (convergeMethod.Method.DeclaringType != context.HandlerContext.Controller.GetType())
+ if (convergeMethod.Method.DeclaringType != context.HandlerContext.Controller?.GetType())
throw new YieldPointException("Converge method must be in the same controller class");
await Task.WhenAll(requests.Select(requestInfo =>
@@ -408,19 +437,19 @@ namespace Tapeti.Flow.Default
}
- public Task AddRequest(TRequest message, Func responseHandler)
+ public Task AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
{
return InternalAddRequest(message, responseHandler);
}
- public Task AddRequest(TRequest message, Func responseHandler)
+ public Task AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class
{
return InternalAddRequest(message, responseHandler);
}
- public Task AddRequestSync(TRequest message, Action responseHandler)
+ public Task AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class
{
return InternalAddRequest(message, responseHandler);
}
@@ -430,6 +459,9 @@ namespace Tapeti.Flow.Default
{
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
+ if (flowContext.ContinuationMetadata == null)
+ throw new InvalidOperationException("No ContinuationMetadata in FlowContext");
+
return flowProvider.SendRequest(
flowContext,
message,
@@ -443,9 +475,17 @@ namespace Tapeti.Flow.Default
internal class ResponseHandlerInfo
{
- public string MethodName { get; set; }
- public string ReplyToQueue { get; set; }
- public bool IsDurableQueue { get; set; }
+ public string MethodName { get; }
+ public string ReplyToQueue { get; }
+ public bool IsDurableQueue { get; }
+
+
+ public ResponseHandlerInfo(string methodName, string replyToQueue, bool isDurableQueue)
+ {
+ MethodName = methodName;
+ ReplyToQueue = replyToQueue;
+ IsDurableQueue = isDurableQueue;
+ }
}
}
}
diff --git a/Tapeti.Flow/Default/FlowStarter.cs b/Tapeti.Flow/Default/FlowStarter.cs
index f391fe8..b8952cc 100644
--- a/Tapeti.Flow/Default/FlowStarter.cs
+++ b/Tapeti.Flow/Default/FlowStarter.cs
@@ -6,7 +6,6 @@ using Tapeti.Config;
namespace Tapeti.Flow.Default
{
- ///
///
/// Default implementation for IFlowStarter.
///
@@ -26,39 +25,38 @@ namespace Tapeti.Flow.Default
///
public async Task Start(Expression>> methodSelector) where TController : class
{
- await CallControllerMethod(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] { });
+ await CallControllerMethod(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), Array.Empty());
}
///
public async Task Start(Expression>>> methodSelector) where TController : class
{
- await CallControllerMethod(GetExpressionMethod(methodSelector), value => (Task)value, new object[] {});
+ await CallControllerMethod(GetExpressionMethod(methodSelector), value => (Task)value, Array.Empty());
}
///
public async Task Start(Expression>> methodSelector, TParameter parameter) where TController : class
{
- await CallControllerMethod(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] {parameter});
+ await CallControllerMethod(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object?[] {parameter});
}
///
public async Task Start(Expression>>> methodSelector, TParameter parameter) where TController : class
{
- await CallControllerMethod(GetExpressionMethod(methodSelector), value => (Task)value, new object[] {parameter});
+ await CallControllerMethod(GetExpressionMethod(methodSelector), value => (Task)value, new object?[] {parameter});
}
- private async Task CallControllerMethod(MethodInfo method, Func> getYieldPointResult, object[] parameters) where TController : class
+ private async Task CallControllerMethod(MethodInfo method, Func> getYieldPointResult, object?[] parameters) where TController : class
{
var controller = config.DependencyResolver.Resolve();
- var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
+ var result = method.Invoke(controller, parameters);
+ if (result == null)
+ throw new InvalidOperationException($"Method {method.Name} must return an IYieldPoint or Task, got null");
- var context = new FlowHandlerContext
- {
- Config = config,
- Controller = controller,
- Method = method
- };
+ var yieldPoint = await getYieldPointResult(result);
+
+ var context = new FlowHandlerContext(config, controller, method);
var flowHandler = config.DependencyResolver.Resolve();
await flowHandler.Execute(context, yieldPoint);
diff --git a/Tapeti.Flow/Default/FlowState.cs b/Tapeti.Flow/Default/FlowState.cs
index eb7961d..763e371 100644
--- a/Tapeti.Flow/Default/FlowState.cs
+++ b/Tapeti.Flow/Default/FlowState.cs
@@ -9,8 +9,8 @@ namespace Tapeti.Flow.Default
///
public class FlowState
{
- private FlowMetadata metadata;
- private Dictionary continuations;
+ private FlowMetadata? metadata;
+ private Dictionary? continuations;
///
@@ -18,7 +18,7 @@ namespace Tapeti.Flow.Default
///
public FlowMetadata Metadata
{
- get => metadata ?? (metadata = new FlowMetadata());
+ get => metadata ??= new FlowMetadata(null);
set => metadata = value;
}
@@ -26,7 +26,7 @@ namespace Tapeti.Flow.Default
///
/// Contains the serialized state which is restored when a flow continues.
///
- public string Data { get; set; }
+ public string? Data { get; set; }
///
@@ -34,7 +34,7 @@ namespace Tapeti.Flow.Default
///
public Dictionary Continuations
{
- get => continuations ?? (continuations = new Dictionary());
+ get => continuations ??= new Dictionary();
set => continuations = value;
}
@@ -45,7 +45,7 @@ namespace Tapeti.Flow.Default
public FlowState Clone()
{
return new FlowState {
- metadata = metadata.Clone(),
+ metadata = metadata?.Clone(),
Data = Data,
continuations = continuations?.ToDictionary(kv => kv.Key, kv => kv.Value.Clone())
};
@@ -61,18 +61,22 @@ namespace Tapeti.Flow.Default
///
/// Contains information about the expected response for this flow.
///
- public ReplyMetadata Reply { get; set; }
+ public ReplyMetadata? Reply { get; }
+ ///
+ public FlowMetadata(ReplyMetadata? reply)
+ {
+ Reply = reply;
+ }
+
+
///
/// Creates a deep clone of this FlowMetadata.
///
public FlowMetadata Clone()
{
- return new FlowMetadata
- {
- Reply = Reply?.Clone()
- };
+ return new FlowMetadata(Reply);
}
}
@@ -85,17 +89,17 @@ namespace Tapeti.Flow.Default
///
/// The queue to which the response should be sent.
///
- public string ReplyTo { get; set; }
+ public string? ReplyTo { get; set; }
///
/// The correlation ID included in the original request.
///
- public string CorrelationId { get; set; }
+ public string? CorrelationId { get; set; }
///
/// The expected response message class.
///
- public string ResponseTypeName { get; set; }
+ public string? ResponseTypeName { get; set; }
///
/// Indicates whether the response should be sent a mandatory.
@@ -128,12 +132,12 @@ namespace Tapeti.Flow.Default
///
/// The name of the method which will handle the response.
///
- public string MethodName { get; set; }
+ public string? MethodName { get; set; }
///
/// The name of the method which is called when all responses have been processed.
///
- public string ConvergeMethodName { get; set; }
+ public string? ConvergeMethodName { get; set; }
///
/// Determines if the converge method is synchronous or asynchronous.
diff --git a/Tapeti.Flow/Default/FlowStore.cs b/Tapeti.Flow/Default/FlowStore.cs
index 044e9d0..3d26ca5 100644
--- a/Tapeti.Flow/Default/FlowStore.cs
+++ b/Tapeti.Flow/Default/FlowStore.cs
@@ -9,7 +9,6 @@ using Tapeti.Flow.FlowHelpers;
namespace Tapeti.Flow.Default
{
- ///
///
/// Default implementation of IFlowStore.
///
@@ -17,11 +16,11 @@ namespace Tapeti.Flow.Default
{
private class CachedFlowState
{
- public readonly FlowState FlowState;
+ public readonly FlowState? FlowState;
public readonly DateTime CreationTime;
public readonly bool IsPersistent;
- public CachedFlowState(FlowState flowState, DateTime creationTime, bool isPersistent)
+ public CachedFlowState(FlowState? flowState, DateTime creationTime, bool isPersistent)
{
FlowState = flowState;
CreationTime = creationTime;
@@ -29,10 +28,10 @@ namespace Tapeti.Flow.Default
}
}
- private readonly ConcurrentDictionary flowStates = new ConcurrentDictionary();
- private readonly ConcurrentDictionary continuationLookup = new ConcurrentDictionary();
- private readonly LockCollection locks = new LockCollection(EqualityComparer.Default);
- private HashSet validatedMethods;
+ private readonly ConcurrentDictionary flowStates = new();
+ private readonly ConcurrentDictionary continuationLookup = new();
+ private readonly LockCollection locks = new(EqualityComparer.Default);
+ private HashSet? validatedMethods;
private readonly IFlowRepository repository;
private readonly ITapetiConfig config;
@@ -51,7 +50,7 @@ namespace Tapeti.Flow.Default
///
- public async Task Load()
+ public async ValueTask Load()
{
if (inUse)
throw new InvalidOperationException("Can only load the saved state once.");
@@ -86,10 +85,13 @@ namespace Tapeti.Flow.Default
private void ValidateContinuation(Guid flowId, Guid continuationId, ContinuationMetadata metadata)
{
+ if (string.IsNullOrEmpty(metadata.MethodName))
+ return;
+
// We could check all the things that are required for a continuation or converge method, but this should suffice
// for the common scenario where you change code without realizing that it's signature has been persisted
// ReSharper disable once InvertIf
- if (validatedMethods.Add(metadata.MethodName))
+ if (validatedMethods!.Add(metadata.MethodName))
{
var methodInfo = MethodSerializer.Deserialize(metadata.MethodName);
if (methodInfo == null)
@@ -114,17 +116,17 @@ namespace Tapeti.Flow.Default
///
- public Task FindFlowID(Guid continuationID)
+ public ValueTask FindFlowID(Guid continuationID)
{
if (!loaded)
throw new InvalidOperationException("Flow store is not yet loaded.");
- return Task.FromResult(continuationLookup.TryGetValue(continuationID, out var result) ? result : (Guid?)null);
+ return new ValueTask(continuationLookup.TryGetValue(continuationID, out var result) ? result : null);
}
///
- public async Task LockFlowState(Guid flowID)
+ public async ValueTask LockFlowState(Guid flowID)
{
if (!loaded)
throw new InvalidOperationException("Flow store should be loaded before storing flows.");
@@ -137,22 +139,22 @@ namespace Tapeti.Flow.Default
///
- public Task> GetActiveFlows(TimeSpan minimumAge)
+ public ValueTask> GetActiveFlows(TimeSpan minimumAge)
{
var maximumDateTime = DateTime.UtcNow - minimumAge;
- return Task.FromResult(flowStates
+ return new ValueTask>(flowStates
.Where(p => p.Value.CreationTime <= maximumDateTime)
.Select(p => new ActiveFlow(p.Key, p.Value.CreationTime))
- .ToArray() as IEnumerable);
+ .ToArray());
}
private class FlowStateLock : IFlowStateLock
{
private readonly FlowStore owner;
- private volatile IDisposable flowLock;
- private CachedFlowState cachedFlowState;
+ private volatile IDisposable? flowLock;
+ private CachedFlowState? cachedFlowState;
public Guid FlowID { get; }
@@ -173,15 +175,15 @@ namespace Tapeti.Flow.Default
l?.Dispose();
}
- public Task GetFlowState()
+ public ValueTask GetFlowState()
{
if (flowLock == null)
throw new ObjectDisposedException("FlowStateLock");
- return Task.FromResult(cachedFlowState?.FlowState?.Clone());
+ return new ValueTask(cachedFlowState?.FlowState?.Clone());
}
- public async Task StoreFlowState(FlowState newFlowState, bool persistent)
+ public async ValueTask StoreFlowState(FlowState newFlowState, bool persistent)
{
if (flowLock == null)
throw new ObjectDisposedException("FlowStateLock");
@@ -190,13 +192,13 @@ namespace Tapeti.Flow.Default
newFlowState = newFlowState.Clone();
// Update the lookup dictionary for the ContinuationIDs
- if (cachedFlowState != null)
+ if (cachedFlowState?.FlowState != null)
{
foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys.Where(k => !newFlowState.Continuations.ContainsKey(k)))
owner.continuationLookup.TryRemove(removedContinuation, out _);
}
- foreach (var addedContinuation in newFlowState.Continuations.Where(c => cachedFlowState == null || !cachedFlowState.FlowState.Continuations.ContainsKey(c.Key)))
+ foreach (var addedContinuation in newFlowState.Continuations.Where(c => cachedFlowState?.FlowState == null || !cachedFlowState.FlowState.Continuations.ContainsKey(c.Key)))
{
owner.continuationLookup.TryAdd(addedContinuation.Key, FlowID);
}
@@ -204,7 +206,7 @@ namespace Tapeti.Flow.Default
var isNew = cachedFlowState == null;
var wasPersistent = cachedFlowState?.IsPersistent ?? false;
- cachedFlowState = new CachedFlowState(newFlowState, isNew ? DateTime.UtcNow : cachedFlowState.CreationTime, persistent);
+ cachedFlowState = new CachedFlowState(newFlowState, isNew ? DateTime.UtcNow : cachedFlowState!.CreationTime, persistent);
owner.flowStates[FlowID] = cachedFlowState;
if (persistent)
@@ -227,12 +229,12 @@ namespace Tapeti.Flow.Default
}
}
- public async Task DeleteFlowState()
+ public async ValueTask DeleteFlowState()
{
if (flowLock == null)
throw new ObjectDisposedException("FlowStateLock");
- if (cachedFlowState != null)
+ if (cachedFlowState?.FlowState != null)
{
foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys)
owner.continuationLookup.TryRemove(removedContinuation, out _);
@@ -240,7 +242,7 @@ namespace Tapeti.Flow.Default
owner.flowStates.TryRemove(FlowID, out var removedFlowState);
cachedFlowState = null;
- if (removedFlowState.IsPersistent)
+ if (removedFlowState is { IsPersistent: true })
await owner.repository.DeleteState(FlowID);
}
}
diff --git a/Tapeti.Flow/Default/NonPersistentFlowRepository.cs b/Tapeti.Flow/Default/NonPersistentFlowRepository.cs
index b1aa283..0ac6ecc 100644
--- a/Tapeti.Flow/Default/NonPersistentFlowRepository.cs
+++ b/Tapeti.Flow/Default/NonPersistentFlowRepository.cs
@@ -5,33 +5,32 @@ using System.Threading.Tasks;
namespace Tapeti.Flow.Default
{
- ///
///
/// Default implementation for IFlowRepository. Does not persist any state, relying on the FlowStore's cache instead.
///
public class NonPersistentFlowRepository : IFlowRepository
{
- Task>> IFlowRepository.GetStates()
+ ValueTask>> IFlowRepository.GetStates()
{
- return Task.FromResult(Enumerable.Empty>());
+ return new ValueTask>>(Enumerable.Empty>());
}
///
- public Task CreateState(Guid flowID, T state, DateTime timestamp)
+ public ValueTask CreateState(Guid flowID, T state, DateTime timestamp)
{
- return Task.CompletedTask;
+ return default;
}
///
- public Task UpdateState(Guid flowID, T state)
+ public ValueTask UpdateState(Guid flowID, T state)
{
- return Task.CompletedTask;
+ return default;
}
///
- public Task DeleteState(Guid flowID)
+ public ValueTask DeleteState(Guid flowID)
{
- return Task.CompletedTask;
+ return default;
}
}
}
diff --git a/Tapeti.Flow/FlowExtension.cs b/Tapeti.Flow/FlowExtension.cs
index 6c979e9..e7610aa 100644
--- a/Tapeti.Flow/FlowExtension.cs
+++ b/Tapeti.Flow/FlowExtension.cs
@@ -4,17 +4,16 @@ using Tapeti.Flow.Default;
namespace Tapeti.Flow
{
- ///
///
/// Provides the Flow middleware.
///
public class FlowExtension : ITapetiExtension
{
- private readonly IFlowRepository flowRepository;
+ private readonly IFlowRepository? flowRepository;
///
///
- public FlowExtension(IFlowRepository flowRepository)
+ public FlowExtension(IFlowRepository? flowRepository)
{
this.flowRepository = flowRepository;
}
diff --git a/Tapeti.Flow/FlowHelpers/LockCollection.cs b/Tapeti.Flow/FlowHelpers/LockCollection.cs
index 309e916..df39769 100644
--- a/Tapeti.Flow/FlowHelpers/LockCollection.cs
+++ b/Tapeti.Flow/FlowHelpers/LockCollection.cs
@@ -7,7 +7,7 @@ namespace Tapeti.Flow.FlowHelpers
///
/// Implementation of an asynchronous locking mechanism.
///
- public class LockCollection
+ public class LockCollection where T : notnull
{
private readonly Dictionary locks;
@@ -57,10 +57,10 @@ namespace Tapeti.Flow.FlowHelpers
private class LockItem : IDisposable
{
- internal volatile LockItem Next;
+ internal volatile LockItem? Next;
private readonly Dictionary locks;
- private readonly TaskCompletionSource tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly T key;
public LockItem(Dictionary locks, T key)
diff --git a/Tapeti.Flow/FlowHelpers/MethodSerializer.cs b/Tapeti.Flow/FlowHelpers/MethodSerializer.cs
index 9f8b09e..38c6f93 100644
--- a/Tapeti.Flow/FlowHelpers/MethodSerializer.cs
+++ b/Tapeti.Flow/FlowHelpers/MethodSerializer.cs
@@ -18,14 +18,14 @@ namespace Tapeti.Flow.FlowHelpers
}
- private static readonly Regex DeserializeRegex = new Regex("^(?.+?)@(?.+?):(?.+?)$");
+ private static readonly Regex DeserializeRegex = new("^(?.+?)@(?.+?):(?.+?)$");
///
/// Deserializes the serialized method representation back into it's MethodInfo, or null if not found.
///
///
- public static MethodInfo Deserialize(string serializedMethod)
+ public static MethodInfo? Deserialize(string serializedMethod)
{
var match = DeserializeRegex.Match(serializedMethod);
if (!match.Success)
@@ -35,8 +35,6 @@ namespace Tapeti.Flow.FlowHelpers
try
{
assembly = Assembly.Load(match.Groups["assembly"].Value);
- if (assembly == null)
- return null;
}
catch
{
diff --git a/Tapeti.Flow/FlowMessageContextPayload.cs b/Tapeti.Flow/FlowMessageContextPayload.cs
index f0cdadb..07476d6 100644
--- a/Tapeti.Flow/FlowMessageContextPayload.cs
+++ b/Tapeti.Flow/FlowMessageContextPayload.cs
@@ -16,9 +16,8 @@ namespace Tapeti.Flow
/// parallel flow is done and the convergeMethod will be called.
/// Temporarily disables storing the flow state.
///
- public bool FlowIsConverging => FlowContext != null &&
- FlowContext.FlowState.Continuations.Count == 0 &&
- FlowContext.ContinuationMetadata.ConvergeMethodName != null;
+ public bool FlowIsConverging => FlowContext.FlowState.Continuations.Count == 0 &&
+ FlowContext.ContinuationMetadata?.ConvergeMethodName != null;
public FlowMessageContextPayload(FlowContext flowContext)
@@ -29,7 +28,7 @@ namespace Tapeti.Flow
public void Dispose()
{
- FlowContext?.Dispose();
+ FlowContext.Dispose();
}
}
}
diff --git a/Tapeti.Flow/IFlowHandlerContext.cs b/Tapeti.Flow/IFlowHandlerContext.cs
index 921dd4e..d0420c5 100644
--- a/Tapeti.Flow/IFlowHandlerContext.cs
+++ b/Tapeti.Flow/IFlowHandlerContext.cs
@@ -4,7 +4,6 @@ using Tapeti.Config;
namespace Tapeti.Flow
{
- ///
///
/// Provides information about the handler for the flow.
///
@@ -19,7 +18,7 @@ namespace Tapeti.Flow
///
/// An instance of the controller which starts or continues the flow.
///
- object Controller { get; }
+ object? Controller { get; }
///
@@ -32,6 +31,6 @@ namespace Tapeti.Flow
/// Access to the message context if this is a continuated flow.
/// Will be null when in a starting flow.
///
- IMessageContext MessageContext { get; }
+ IMessageContext? MessageContext { get; }
}
}
diff --git a/Tapeti.Flow/IFlowProvider.cs b/Tapeti.Flow/IFlowProvider.cs
index df8a485..613e82d 100644
--- a/Tapeti.Flow/IFlowProvider.cs
+++ b/Tapeti.Flow/IFlowProvider.cs
@@ -20,7 +20,19 @@ namespace Tapeti.Flow
///
///
///
- IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler);
+ IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler) where TRequest : class where TResponse : class;
+
+
+ ///
+ /// Publish a request message and continue the flow when the response arrives.
+ /// The request message must be marked with the [Request] attribute, and the
+ /// Response type must match. Used for asynchronous response handlers.
+ ///
+ ///
+ ///
+ ///
+ ///
+ IYieldPoint YieldWithRequest(TRequest message, Func> responseHandler) where TRequest : class where TResponse : class;
///
@@ -39,7 +51,7 @@ namespace Tapeti.Flow
///
///
///
- IYieldPoint YieldWithRequestSync(TRequest message, Func responseHandler);
+ IYieldPoint YieldWithRequestSync(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
///
@@ -55,7 +67,7 @@ namespace Tapeti.Flow
///
///
///
- IYieldPoint EndWithResponse(TResponse message);
+ IYieldPoint EndWithResponse(TResponse message) where TResponse : class;
///
@@ -108,19 +120,19 @@ namespace Tapeti.Flow
///
///
///
- Task Execute(IFlowHandlerContext context, IYieldPoint yieldPoint);
+ ValueTask Execute(IFlowHandlerContext context, IYieldPoint yieldPoint);
///
/// Returns the parallel request for the given message context.
///
- IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context);
+ IFlowParallelRequest? GetParallelRequest(IFlowHandlerContext context);
///
/// Calls the converge method for a parallel flow.
///
- Task Converge(IFlowHandlerContext context);
+ ValueTask Converge(IFlowHandlerContext context);
}
@@ -162,14 +174,31 @@ namespace Tapeti.Flow
///
///
///
- IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler);
+ IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
+
+ ///
+ /// Publish a request message and continue the flow when the response arrives.
+ /// Note that the response handler can not influence the flow as it does not return a YieldPoint.
+ /// It can instead store state in the controller for the continuation passed to the Yield method.
+ /// Used for asynchronous response handlers.
+ ///
+ ///
+ ///
+ IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
///
/// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
///
///
- IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler);
+ IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
+
+ ///
+ /// This overload allows the response handler access to the IFlowParallelRequest interface, which
+ /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
+ ///
+ ///
+ IFlowParallelRequestBuilder AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
///
/// Publish a request message and continue the flow when the response arrives.
@@ -179,7 +208,14 @@ namespace Tapeti.Flow
///
///
///
- IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler);
+ IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class;
+
+ ///
+ /// This overload allows the response handler access to the IFlowParallelRequest interface, which
+ /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
+ ///
+ ///
+ IFlowParallelRequestBuilder AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class;
/// There is no Sync overload with an IFlowParallelRequest parameter, as the AddRequest methods for that are
/// async, so you should always await them.
@@ -224,14 +260,14 @@ namespace Tapeti.Flow
///
///
///
- Task AddRequest(TRequest message, Func responseHandler);
+ Task AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
///
/// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
///
///
- Task AddRequest(TRequest message, Func responseHandler);
+ Task AddRequest(TRequest message, Func responseHandler) where TRequest : class where TResponse : class;
///
/// Publish a request message and continue the flow when the response arrives.
@@ -241,7 +277,7 @@ namespace Tapeti.Flow
///
///
///
- Task AddRequestSync(TRequest message, Action responseHandler);
+ Task AddRequestSync(TRequest message, Action responseHandler) where TRequest : class where TResponse : class;
}
diff --git a/Tapeti.Flow/IFlowRepository.cs b/Tapeti.Flow/IFlowRepository.cs
index cde801c..691279b 100644
--- a/Tapeti.Flow/IFlowRepository.cs
+++ b/Tapeti.Flow/IFlowRepository.cs
@@ -13,7 +13,7 @@ namespace Tapeti.Flow
/// Load the previously persisted flow states.
///
/// A list of flow states, where the key is the unique Flow ID and the value is the deserialized T.
- Task>> GetStates();
+ ValueTask>> GetStates();
///
/// Stores a new flow state. Guaranteed to be run in a lock for the specified flow ID.
@@ -22,20 +22,20 @@ namespace Tapeti.Flow
/// The flow state to be stored.
/// The time when the flow was initially created.
///
- Task CreateState(Guid flowID, T state, DateTime timestamp);
+ ValueTask CreateState(Guid flowID, T state, DateTime timestamp);
///
/// Updates an existing flow state. Guaranteed to be run in a lock for the specified flow ID.
///
/// The unique ID of the flow.
/// The flow state to be stored.
- Task UpdateState(Guid flowID, T state);
+ ValueTask UpdateState(Guid flowID, T state);
///
/// Delete a flow state. Guaranteed to be run in a lock for the specified flow ID.
///
/// The unique ID of the flow.
- Task DeleteState(Guid flowID);
+ ValueTask DeleteState(Guid flowID);
}
diff --git a/Tapeti.Flow/IFlowStore.cs b/Tapeti.Flow/IFlowStore.cs
index b3720b3..4c68a8f 100644
--- a/Tapeti.Flow/IFlowStore.cs
+++ b/Tapeti.Flow/IFlowStore.cs
@@ -17,19 +17,19 @@ namespace Tapeti.Flow
/// If using an IFlowRepository that requires an update (such as creating tables) make
/// sure it is called before calling Load.
///
- Task Load();
+ ValueTask Load();
///
/// Looks up the FlowID corresponding to a ContinuationID. For internal use.
///
///
- Task FindFlowID(Guid continuationID);
+ ValueTask FindFlowID(Guid continuationID);
///
/// Acquires a lock on the flow with the specified FlowID.
///
///
- Task LockFlowState(Guid flowID);
+ ValueTask LockFlowState(Guid flowID);
///
/// Returns information about the currently active flows.
@@ -38,11 +38,10 @@ namespace Tapeti.Flow
/// This is intended for monitoring purposes and should be treated as a snapshot.
///
/// The minimum age of the flow before it is included in the result. Set to TimeSpan.Zero to return all active flows.
- Task> GetActiveFlows(TimeSpan minimumAge);
+ ValueTask> GetActiveFlows(TimeSpan minimumAge);
}
- ///
///
/// Represents a lock on the flow state, to provide thread safety.
///
@@ -56,19 +55,19 @@ namespace Tapeti.Flow
///
/// Acquires a copy of the flow state.
///
- Task GetFlowState();
+ ValueTask GetFlowState();
///
/// Stores the new flow state.
///
///
///
- Task StoreFlowState(FlowState flowState, bool persistent);
+ ValueTask StoreFlowState(FlowState flowState, bool persistent);
///
/// Disposes of the flow state corresponding to this Flow ID.
///
- Task DeleteFlowState();
+ ValueTask DeleteFlowState();
}
diff --git a/Tapeti.Flow/ReSharper/JetBrains.Annotations.cs b/Tapeti.Flow/ReSharper/JetBrains.Annotations.cs
index ef47d40..39940b0 100644
--- a/Tapeti.Flow/ReSharper/JetBrains.Annotations.cs
+++ b/Tapeti.Flow/ReSharper/JetBrains.Annotations.cs
@@ -170,11 +170,11 @@ namespace JetBrains.Annotations
{
public PublicAPIAttribute() { }
- public PublicAPIAttribute([NotNull] string comment)
+ public PublicAPIAttribute(string comment)
{
Comment = comment;
}
- [CanBeNull] public string Comment { get; }
+ public string? Comment { get; }
}
}
\ No newline at end of file
diff --git a/Tapeti.Flow/ResponseExpectedException.cs b/Tapeti.Flow/ResponseExpectedException.cs
index 07d66b1..8c0d225 100644
--- a/Tapeti.Flow/ResponseExpectedException.cs
+++ b/Tapeti.Flow/ResponseExpectedException.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Flow
{
- ///
///
/// Raised when a response is expected to end a flow, but none was provided.
///
diff --git a/Tapeti.Flow/Tapeti.Flow.csproj b/Tapeti.Flow/Tapeti.Flow.csproj
index 1ade44f..b772e71 100644
--- a/Tapeti.Flow/Tapeti.Flow.csproj
+++ b/Tapeti.Flow/Tapeti.Flow.csproj
@@ -1,7 +1,7 @@
-
+
- netstandard2.0
+ net6.0;net7.0
true
Menno van Lavieren, Mark van Renswoude
@@ -11,12 +11,19 @@
https://github.com/MvRens/Tapeti
Tapeti.Flow.png
2.0.0
+ 9
+ enable
1701;1702
+
+
+ IDE0066
+
+
@@ -29,7 +36,7 @@
-
-
+
+
diff --git a/Tapeti.Flow/YieldPointException.cs b/Tapeti.Flow/YieldPointException.cs
index 2d135d0..a0eed51 100644
--- a/Tapeti.Flow/YieldPointException.cs
+++ b/Tapeti.Flow/YieldPointException.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Flow
{
- ///
///
/// Raised when an invalid yield point is returned.
///
diff --git a/Tapeti.Ninject/NinjectDependencyResolver.cs b/Tapeti.Ninject/NinjectDependencyResolver.cs
index 0e2fdcf..4f2b791 100644
--- a/Tapeti.Ninject/NinjectDependencyResolver.cs
+++ b/Tapeti.Ninject/NinjectDependencyResolver.cs
@@ -4,7 +4,6 @@ using Ninject;
namespace Tapeti.Ninject
{
- ///
///
/// Dependency resolver and container implementation for Ninject.
///
@@ -49,7 +48,7 @@ namespace Tapeti.Ninject
if (kernel.GetBindings(typeof(TService)).Any())
return;
- kernel.Bind().ToMethod(context => factory());
+ kernel.Bind().ToMethod(_ => factory());
}
@@ -77,7 +76,7 @@ namespace Tapeti.Ninject
if (kernel.GetBindings(typeof(TService)).Any())
return;
- kernel.Bind().ToMethod(context => factory()).InSingletonScope();
+ kernel.Bind().ToMethod(_ => factory()).InSingletonScope();
}
diff --git a/Tapeti.Ninject/Tapeti.Ninject.csproj b/Tapeti.Ninject/Tapeti.Ninject.csproj
index 994a749..d31c98b 100644
--- a/Tapeti.Ninject/Tapeti.Ninject.csproj
+++ b/Tapeti.Ninject/Tapeti.Ninject.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Mark van Renswoude
@@ -11,10 +11,12 @@
https://github.com/MvRens/Tapeti
Tapeti.SimpleInjector.png
2.0.0
+ 9
+ enable
-
+
@@ -29,6 +31,6 @@
-
+
diff --git a/Tapeti.Serilog/Default/DiagnosticContext.cs b/Tapeti.Serilog/Default/DiagnosticContext.cs
index 387ebe1..0f7f291 100644
--- a/Tapeti.Serilog/Default/DiagnosticContext.cs
+++ b/Tapeti.Serilog/Default/DiagnosticContext.cs
@@ -10,7 +10,7 @@ namespace Tapeti.Serilog.Default
public class DiagnosticContext : IDiagnosticContext
{
private readonly global::Serilog.ILogger logger;
- private readonly List properties = new List();
+ private readonly List properties = new();
///
diff --git a/Tapeti.Serilog/Middleware/MessageHandlerLoggingBindingMiddleware.cs b/Tapeti.Serilog/Middleware/MessageHandlerLoggingBindingMiddleware.cs
index d3ba51d..17a1c28 100644
--- a/Tapeti.Serilog/Middleware/MessageHandlerLoggingBindingMiddleware.cs
+++ b/Tapeti.Serilog/Middleware/MessageHandlerLoggingBindingMiddleware.cs
@@ -43,7 +43,7 @@ namespace Tapeti.Serilog.Middleware
}
- private static object DiagnosticContextFactory(IMessageContext context)
+ private static object? DiagnosticContextFactory(IMessageContext context)
{
return context.TryGet(out var diagnosticContextPayload)
? diagnosticContextPayload.DiagnosticContext
diff --git a/Tapeti.Serilog/Middleware/MessageHandlerLoggingMessageMiddleware.cs b/Tapeti.Serilog/Middleware/MessageHandlerLoggingMessageMiddleware.cs
index c8e6e73..1043039 100644
--- a/Tapeti.Serilog/Middleware/MessageHandlerLoggingMessageMiddleware.cs
+++ b/Tapeti.Serilog/Middleware/MessageHandlerLoggingMessageMiddleware.cs
@@ -29,7 +29,7 @@ namespace Tapeti.Serilog.Middleware
}
///
- public async Task Handle(IMessageContext context, Func next)
+ public async ValueTask Handle(IMessageContext context, Func next)
{
var logger = context.Config.DependencyResolver.Resolve();
@@ -41,6 +41,7 @@ namespace Tapeti.Serilog.Middleware
await next();
+
stopwatch.Stop();
diff --git a/Tapeti.Serilog/Tapeti.Serilog.csproj b/Tapeti.Serilog/Tapeti.Serilog.csproj
index 73171f4..ddef97e 100644
--- a/Tapeti.Serilog/Tapeti.Serilog.csproj
+++ b/Tapeti.Serilog/Tapeti.Serilog.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Hans Mulder, Mark van Renswoude
@@ -11,6 +11,8 @@
https://github.com/MvRens/Tapeti
Tapeti.Serilog.png
2.0.0
+ 9
+ enable
@@ -18,7 +20,7 @@
-
+
@@ -33,6 +35,6 @@
-
+
diff --git a/Tapeti.Serilog/TapetiSeriLogger.cs b/Tapeti.Serilog/TapetiSeriLogger.cs
index 98c7864..dfdd830 100644
--- a/Tapeti.Serilog/TapetiSeriLogger.cs
+++ b/Tapeti.Serilog/TapetiSeriLogger.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text;
using Tapeti.Config;
+using Tapeti.Connection;
using ISerilogLogger = Serilog.ILogger;
// ReSharper disable UnusedMember.Global
@@ -86,7 +87,7 @@ namespace Tapeti.Serilog
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
var message = new StringBuilder("Tapeti: exception in message handler");
- var messageParams = new List();
+ var messageParams = new List();
var contextLogger = seriLogger
.ForContext("consumeResult", consumeResult)
@@ -129,10 +130,11 @@ namespace Tapeti.Serilog
}
///
- public void QueueExistsWarning(string queueName, Dictionary arguments)
+ public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? 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,
+ existingArguments,
arguments);
}
diff --git a/Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs b/Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs
index c8a189f..0960843 100644
--- a/Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs
+++ b/Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs
@@ -4,19 +4,18 @@ using SimpleInjector;
namespace Tapeti.SimpleInjector
{
- ///
///
/// Dependency resolver and container implementation for SimpleInjector.
///
public class SimpleInjectorDependencyResolver : IDependencyContainer
{
private readonly Container container;
- private readonly Lifestyle defaultsLifestyle;
- private readonly Lifestyle controllersLifestyle;
+ private readonly Lifestyle? defaultsLifestyle;
+ private readonly Lifestyle? controllersLifestyle;
///
///
- public SimpleInjectorDependencyResolver(Container container, Lifestyle defaultsLifestyle = null, Lifestyle controllersLifestyle = null)
+ public SimpleInjectorDependencyResolver(Container container, Lifestyle? defaultsLifestyle = null, Lifestyle? controllersLifestyle = null)
{
this.container = container;
this.defaultsLifestyle = defaultsLifestyle;
diff --git a/Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj b/Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj
index de35d46..c0c5614 100644
--- a/Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj
+++ b/Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Mark van Renswoude
@@ -11,7 +11,8 @@
https://github.com/MvRens/Tapeti
Tapeti.SimpleInjector.png
2.0.0
- 2.0.0
+ 9
+ enable
@@ -19,7 +20,7 @@
-
+
@@ -34,6 +35,6 @@
-
+
diff --git a/Tapeti.Tests/Client/RabbitMQFixture.cs b/Tapeti.Tests/Client/RabbitMQFixture.cs
new file mode 100644
index 0000000..4d1c66d
--- /dev/null
+++ b/Tapeti.Tests/Client/RabbitMQFixture.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Threading.Tasks;
+using Docker.DotNet;
+using Docker.DotNet.Models;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using DotNet.Testcontainers.Containers;
+using Xunit;
+
+namespace Tapeti.Tests.Client
+{
+ [CollectionDefinition(Name)]
+ public sealed class RabbitMQCollection : ICollectionFixture
+ {
+ public const string Name = "RabbitMQ";
+ }
+
+
+ public sealed class RabbitMQFixture : IAsyncLifetime
+ {
+ public static string RabbitMQUsername => "tapetitests";
+ public static string RabbitMQPassword => "topsecret1234";
+
+ public ushort RabbitMQPort { get; private set; }
+ public ushort RabbitMQManagementPort { get; private set; }
+
+
+ private TestcontainerMessageBroker? testcontainers;
+
+ private const int DefaultRabbitMQPort = 5672;
+ private const int DefaultRabbitMQManagementPort = 15672;
+
+
+
+ private const string ImageName = "rabbitmq";
+ private const string ImageTag = "3.11.3-alpine";
+
+
+ public async Task InitializeAsync()
+ {
+ // Testcontainers does not seem to pull the image the first time.
+ // I didn't get it to work, even using WithImagePullPolicy from the latest beta.
+ // Note: running it the first time can take a while.
+ var client = new DockerClientConfiguration().CreateClient();
+ await client.Images.CreateImageAsync(
+ new ImagesCreateParameters
+ {
+ FromImage = ImageName,
+ Tag = ImageTag
+ },
+ null,
+ new Progress());
+
+ // If you get a "Sequence contains no elements" error here: make sure Docker Desktop is running
+ var testcontainersBuilder = new TestcontainersBuilder()
+ .WithMessageBroker(new RabbitMqTestcontainerConfiguration($"{ImageName}:{ImageTag}")
+ {
+ Username = RabbitMQUsername,
+ Password = RabbitMQPassword
+ })
+ .WithExposedPort(DefaultRabbitMQManagementPort)
+ .WithPortBinding(0, DefaultRabbitMQManagementPort);
+
+ testcontainers = testcontainersBuilder.Build();
+
+ await testcontainers.StartAsync();
+
+ RabbitMQPort = testcontainers.GetMappedPublicPort(DefaultRabbitMQPort);
+ RabbitMQManagementPort = testcontainers.GetMappedPublicPort(DefaultRabbitMQManagementPort);
+ }
+
+
+ public async Task DisposeAsync()
+ {
+ if (testcontainers != null)
+ await testcontainers.DisposeAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Client/TapetiClientTests.cs b/Tapeti.Tests/Client/TapetiClientTests.cs
new file mode 100644
index 0000000..bfd1cfe
--- /dev/null
+++ b/Tapeti.Tests/Client/TapetiClientTests.cs
@@ -0,0 +1,116 @@
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Tapeti.Connection;
+using Tapeti.Default;
+using Tapeti.Exceptions;
+using Tapeti.Tests.Mock;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Tapeti.Tests.Client
+{
+ [Collection(RabbitMQCollection.Name)]
+ [Trait("Category", "Requires Docker")]
+ public class TapetiClientTests : IAsyncLifetime
+ {
+ private readonly RabbitMQFixture fixture;
+ private readonly MockDependencyResolver dependencyResolver = new();
+
+ private TapetiClient client = null!;
+
+
+ public TapetiClientTests(RabbitMQFixture fixture, ITestOutputHelper testOutputHelper)
+ {
+ this.fixture = fixture;
+
+ dependencyResolver.Set(new MockLogger(testOutputHelper));
+ }
+
+
+ public Task InitializeAsync()
+ {
+ client = CreateClient();
+
+ return Task.CompletedTask;
+ }
+
+
+ public async Task DisposeAsync()
+ {
+ await client.Close();
+ }
+
+
+
+ [Fact]
+ public void Fixture()
+ {
+ fixture.RabbitMQPort.Should().BeGreaterThan(0);
+ fixture.RabbitMQManagementPort.Should().BeGreaterThan(0);
+ }
+
+
+ [Fact]
+ public async Task DynamicQueueDeclareNoPrefix()
+ {
+ var queueName = await client.DynamicQueueDeclare(null, null, CancellationToken.None);
+ queueName.Should().NotBeNullOrEmpty();
+ }
+
+
+ [Fact]
+ public async Task DynamicQueueDeclarePrefix()
+ {
+ var queueName = await client.DynamicQueueDeclare("dynamicprefix", null, CancellationToken.None);
+ queueName.Should().StartWith("dynamicprefix");
+ }
+
+
+ [Fact]
+ public async Task PublishHandleOverflow()
+ {
+ var queue1 = await client.DynamicQueueDeclare(null, new RabbitMQArguments
+ {
+ { "x-max-length", 5 },
+ { "x-overflow", "reject-publish" }
+ }, CancellationToken.None);
+
+ var queue2 = await client.DynamicQueueDeclare(null, null, CancellationToken.None);
+
+ var body = Encoding.UTF8.GetBytes("Hello world!");
+ var properties = new MessageProperties();
+
+
+ for (var i = 0; i < 5; i++)
+ await client.Publish(body, properties, null, queue1, true);
+
+
+ var publishOverMaxLength = () => client.Publish(body, properties, null, queue1, true);
+ await publishOverMaxLength.Should().ThrowAsync();
+
+ // The channel should recover and allow further publishing
+ await client.Publish(body, properties, null, queue2, true);
+ }
+
+
+ // TODO test the other methods
+
+
+ private TapetiClient CreateClient()
+ {
+ return new TapetiClient(
+ new TapetiConfig.Config(dependencyResolver),
+ new TapetiConnectionParams
+ {
+ HostName = "127.0.0.1",
+ Port = fixture.RabbitMQPort,
+ ManagementPort = fixture.RabbitMQManagementPort,
+ Username = RabbitMQFixture.RabbitMQUsername,
+ Password = RabbitMQFixture.RabbitMQPassword,
+ PrefetchCount = 50
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Config/BaseControllerTest.cs b/Tapeti.Tests/Config/BaseControllerTest.cs
new file mode 100644
index 0000000..aba1651
--- /dev/null
+++ b/Tapeti.Tests/Config/BaseControllerTest.cs
@@ -0,0 +1,29 @@
+using JetBrains.Annotations;
+using Tapeti.Config;
+using Tapeti.Tests.Mock;
+
+namespace Tapeti.Tests.Config
+{
+ public class BaseControllerTest
+ {
+ protected readonly MockDependencyResolver DependencyResolver = new();
+
+
+ protected ITapetiConfig GetControllerConfig<[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] T>() where T : class
+ {
+ var configBuilder = new TapetiConfig(DependencyResolver);
+
+ configBuilder.EnableDeclareDurableQueues();
+ configBuilder.RegisterController(typeof(T));
+ var config = configBuilder.Build();
+
+ return config;
+ }
+
+
+ protected ITapetiConfigBindings GetControllerBindings<[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] T>() where T : class
+ {
+ return GetControllerConfig().Bindings;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Config/QueueArgumentsTest.cs b/Tapeti.Tests/Config/QueueArgumentsTest.cs
new file mode 100644
index 0000000..98a74c2
--- /dev/null
+++ b/Tapeti.Tests/Config/QueueArgumentsTest.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Moq;
+using Tapeti.Annotations;
+using Tapeti.Config;
+using Tapeti.Connection;
+using Xunit;
+
+namespace Tapeti.Tests.Config
+{
+ internal static class UTF8StringExtensions
+ {
+ public static string AsUTF8String(this object value)
+ {
+ value.Should().BeOfType();
+ return Encoding.UTF8.GetString((byte[])value);
+ }
+ }
+
+
+ public class QueueArgumentsTest : BaseControllerTest
+ {
+ private static readonly MockRepository MoqRepository = new(MockBehavior.Strict);
+
+ private readonly Mock client;
+ private readonly Dictionary declaredQueues = new();
+
+
+ public QueueArgumentsTest()
+ {
+ client = MoqRepository.Create();
+ var routingKeyStrategy = MoqRepository.Create();
+ var exchangeStrategy = MoqRepository.Create();
+
+ DependencyResolver.Set(routingKeyStrategy.Object);
+ DependencyResolver.Set(exchangeStrategy.Object);
+
+
+ routingKeyStrategy
+ .Setup(s => s.GetRoutingKey(typeof(TestMessage1)))
+ .Returns("testmessage1");
+
+ routingKeyStrategy
+ .Setup(s => s.GetRoutingKey(typeof(TestMessage2)))
+ .Returns("testmessage2");
+
+ exchangeStrategy
+ .Setup(s => s.GetExchange(It.IsAny()))
+ .Returns("exchange");
+
+ var queue = 0;
+ client
+ .Setup(c => c.DynamicQueueDeclare(null, It.IsAny(), It.IsAny()))
+ .Callback((string _, IRabbitMQArguments arguments, CancellationToken _) =>
+ {
+ queue++;
+ declaredQueues.Add($"queue-{queue}", arguments);
+ })
+ .ReturnsAsync(() => $"queue-{queue}");
+
+ client
+ .Setup(c => c.DurableQueueDeclare(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Callback((string queueName, IEnumerable _, IRabbitMQArguments arguments, CancellationToken _) =>
+ {
+ declaredQueues.Add(queueName, arguments);
+ })
+ .Returns(Task.CompletedTask);
+
+
+ client
+ .Setup(c => c.DynamicQueueBind(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+ }
+
+
+ [Fact]
+ public async Task SingleQueueArguments()
+ {
+ var config = GetControllerConfig();
+
+ var binding1 = config.Bindings.Single(b => b is IControllerMethodBinding cmb && cmb.Method.Name == "HandleMessage1");
+ binding1.Should().NotBeNull();
+
+ var binding2 = config.Bindings.Single(b => b is IControllerMethodBinding cmb && cmb.Method.Name == "HandleMessage2");
+ binding2.Should().NotBeNull();
+
+
+
+ var subscriber = new TapetiSubscriber(() => client.Object, config);
+ await subscriber.ApplyBindings();
+
+
+ declaredQueues.Should().HaveCount(1);
+ var arguments = declaredQueues["queue-1"];
+
+ arguments.Should().ContainKey("x-custom").WhoseValue.AsUTF8String().Should().Be("custom value");
+ arguments.Should().ContainKey("x-another").WhoseValue.Should().Be(true);
+ arguments.Should().ContainKey("x-max-length").WhoseValue.Should().Be(100);
+ arguments.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be(100000);
+ arguments.Should().ContainKey("x-message-ttl").WhoseValue.Should().Be(4269);
+ arguments.Should().ContainKey("x-overflow").WhoseValue.AsUTF8String().Should().Be("reject-publish");
+ }
+
+
+ [Fact]
+ public async Task ConflictingDynamicQueueArguments()
+ {
+ var config = GetControllerConfig();
+
+ var subscriber = new TapetiSubscriber(() => client.Object, config);
+ await subscriber.ApplyBindings();
+
+ declaredQueues.Should().HaveCount(2);
+
+ var arguments1 = declaredQueues["queue-1"];
+ arguments1.Should().ContainKey("x-max-length").WhoseValue.Should().Be(100);
+
+ var arguments2 = declaredQueues["queue-2"];
+ arguments2.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be(100000);
+ }
+
+
+ [Fact]
+ public async Task ConflictingDurableQueueArguments()
+ {
+ var config = GetControllerConfig();
+
+ var testApplyBindings = () =>
+ {
+ var subscriber = new TapetiSubscriber(() => client.Object, config);
+ return subscriber.ApplyBindings();
+ };
+
+ using (new AssertionScope())
+ {
+ await testApplyBindings.Should().ThrowAsync();
+ declaredQueues.Should().HaveCount(0);
+ }
+ }
+
+
+ // ReSharper disable all
+ #pragma warning disable
+
+ private class TestMessage1
+ {
+ }
+
+
+ private class TestMessage2
+ {
+ }
+
+
+ [DynamicQueue]
+ [QueueArguments("x-custom", "custom value", "x-another", true, MaxLength = 100, MaxLengthBytes = 100000, MessageTTL = 4269, Overflow = RabbitMQOverflow.RejectPublish)]
+ private class TestController
+ {
+ public void HandleMessage1(TestMessage1 message)
+ {
+ }
+
+
+ public void HandleMessage2(TestMessage2 message)
+ {
+ }
+ }
+
+
+ [DynamicQueue]
+ [QueueArguments(MaxLength = 100)]
+ private class ConflictingArgumentsTestController
+ {
+ public void HandleMessage1(TestMessage1 message)
+ {
+ }
+
+
+ [QueueArguments(MaxLengthBytes = 100000)]
+ public void HandleMessage2(TestMessage1 message)
+ {
+ }
+ }
+
+
+ [DurableQueue("durable")]
+ [QueueArguments(MaxLength = 100)]
+ private class ConflictingArgumentsDurableQueueTestController
+ {
+ public void HandleMessage1(TestMessage1 message)
+ {
+ }
+
+
+ [QueueArguments(MaxLengthBytes = 100000)]
+ public void HandleMessage2(TestMessage1 message)
+ {
+ }
+ }
+
+ #pragma warning restore
+ // ReSharper restore all
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Config/SimpleControllerTest.cs b/Tapeti.Tests/Config/SimpleControllerTest.cs
new file mode 100644
index 0000000..8dca8ad
--- /dev/null
+++ b/Tapeti.Tests/Config/SimpleControllerTest.cs
@@ -0,0 +1,54 @@
+using System.Linq;
+using FluentAssertions;
+using Tapeti.Annotations;
+using Tapeti.Config;
+using Xunit;
+
+namespace Tapeti.Tests.Config
+{
+ public class SimpleControllerTest : BaseControllerTest
+ {
+ [Fact]
+ public void RegisterController()
+ {
+ var bindings = GetControllerBindings();
+ bindings.Should().HaveCount(2);
+
+ var handleSimpleMessageBinding = bindings.Single(b => b is IControllerMethodBinding cmb &&
+ cmb.Controller == typeof(TestController) &&
+ cmb.Method.Name == "HandleSimpleMessage");
+ handleSimpleMessageBinding.QueueType.Should().Be(QueueType.Dynamic);
+
+
+ var handleSimpleMessageStaticBinding = bindings.Single(b => b is IControllerMethodBinding cmb &&
+ cmb.Controller == typeof(TestController) &&
+ cmb.Method.Name == "HandleSimpleMessageStatic");
+ handleSimpleMessageStaticBinding.QueueType.Should().Be(QueueType.Dynamic);
+
+ }
+
+
+ // ReSharper disable all
+ #pragma warning disable
+
+ private class TestMessage
+ {
+ }
+
+
+ [DynamicQueue]
+ private class TestController
+ {
+ public void HandleSimpleMessage(TestMessage message)
+ {
+ }
+
+ public static void HandleSimpleMessageStatic(TestMessage message)
+ {
+ }
+ }
+
+ #pragma warning restore
+ // ReSharper restore all
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Helpers/ConnectionStringParser.cs b/Tapeti.Tests/Helpers/ConnectionStringParserTest.cs
similarity index 100%
rename from Tapeti.Tests/Helpers/ConnectionStringParser.cs
rename to Tapeti.Tests/Helpers/ConnectionStringParserTest.cs
diff --git a/Tapeti.Tests/Helpers/ExpressionInvokerTest.cs b/Tapeti.Tests/Helpers/ExpressionInvokerTest.cs
new file mode 100644
index 0000000..0771dd1
--- /dev/null
+++ b/Tapeti.Tests/Helpers/ExpressionInvokerTest.cs
@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using FluentAssertions;
+using Tapeti.Helpers;
+using Xunit;
+
+namespace Tapeti.Tests.Helpers
+{
+ public class ExpressionInvokerTest
+ {
+ [Fact]
+ public void InstanceMethodVoidNoParameters()
+ {
+ const string methodName = nameof(InvokeTarget.InstanceMethodVoidNoParameters);
+ var invoker = InvokerFor(methodName);
+
+ var target = new InvokeTarget();
+ invoker.Invoke(target);
+
+ target.Verify(methodName);
+ }
+
+
+ [Fact]
+ public void InstanceMethodReturnValueNoParameters()
+ {
+ const string methodName = nameof(InvokeTarget.InstanceMethodReturnValueNoParameters);
+ var invoker = InvokerFor(methodName);
+
+ var target = new InvokeTarget();
+ var returnValue = invoker.Invoke(target);
+
+ target.Verify(methodName);
+ returnValue.Should().Be("Hello world!");
+ }
+
+
+ [Fact]
+ public void InstanceMethodVoidParameters()
+ {
+ const string methodName = nameof(InvokeTarget.InstanceMethodVoidParameters);
+ var invoker = InvokerFor(methodName);
+
+ var target = new InvokeTarget();
+ invoker.Invoke(target, 42);
+
+ target.Verify(methodName, "42");
+ }
+
+
+ [Fact]
+ public void InstanceMethodReturnValueParameters()
+ {
+ const string methodName = nameof(InvokeTarget.InstanceMethodReturnValueParameters);
+ var invoker = InvokerFor(methodName);
+
+ var target = new InvokeTarget();
+ var returnValue = invoker.Invoke(target, new byte[] { 42, 69 });
+
+ target.Verify(methodName, "42,69");
+ returnValue.Should().Be(true);
+ }
+
+
+ [Fact]
+ public void StaticMethodVoidNoParameters()
+ {
+ InvokeTarget.ResetStatic();
+
+ const string methodName = nameof(InvokeTarget.StaticMethodVoidNoParameters);
+ var invoker = InvokerFor(methodName);
+
+ invoker.Invoke(null);
+
+ InvokeTarget.VerifyStatic(methodName);
+ }
+
+
+ [Fact]
+ public void StaticMethodReturnValueNoParameters()
+ {
+ InvokeTarget.ResetStatic();
+
+ const string methodName = nameof(InvokeTarget.StaticMethodReturnValueNoParameters);
+ var invoker = InvokerFor(methodName);
+
+ var returnValue = invoker.Invoke(null);
+
+ InvokeTarget.VerifyStatic(methodName);
+ returnValue.Should().Be("Hello world!");
+ }
+
+
+ [Fact]
+ public void StaticMethodVoidParameters()
+ {
+ InvokeTarget.ResetStatic();
+
+ const string methodName = nameof(InvokeTarget.StaticMethodVoidParameters);
+ var invoker = InvokerFor(methodName);
+
+ invoker.Invoke(null, 42);
+
+ InvokeTarget.VerifyStatic(methodName, "42");
+ }
+
+
+ [Fact]
+ public void StaticMethodReturnValueParameters()
+ {
+ InvokeTarget.ResetStatic();
+
+ const string methodName = nameof(InvokeTarget.StaticMethodReturnValueParameters);
+ var invoker = InvokerFor(methodName);
+
+ var returnValue = invoker.Invoke(null, new byte[] { 42, 69 });
+
+ InvokeTarget.VerifyStatic(methodName, "42,69");
+ returnValue.Should().Be(true);
+ }
+
+
+ private static ExpressionInvoke InvokerFor(string invokeTargetMethodName)
+ {
+ var method = typeof(InvokeTarget).GetMethod(invokeTargetMethodName);
+ return method!.CreateExpressionInvoke();
+ }
+
+
+
+ // ReSharper disable ParameterHidesMember
+ private class InvokeTarget
+ {
+ private static string? staticMethodName;
+ private static string? staticParameters;
+
+ private string? methodName;
+ private string? parameters;
+
+
+ public void InstanceMethodVoidNoParameters()
+ {
+ MethodCalled();
+ }
+
+ public string InstanceMethodReturnValueNoParameters()
+ {
+ MethodCalled();
+ return "Hello world!";
+ }
+
+ public void InstanceMethodVoidParameters(int answer)
+ {
+ MethodCalled(answer.ToString());
+ }
+
+ public bool InstanceMethodReturnValueParameters(IEnumerable values)
+ {
+ MethodCalled(string.Join(',', values.Select(v => v.ToString())));
+ return true;
+ }
+
+
+ public static void StaticMethodVoidNoParameters()
+ {
+ StaticMethodCalled();
+ }
+
+ public static string StaticMethodReturnValueNoParameters()
+ {
+ StaticMethodCalled();
+ return "Hello world!";
+ }
+
+ public static void StaticMethodVoidParameters(int answer)
+ {
+ StaticMethodCalled(answer.ToString());
+ }
+
+ public static bool StaticMethodReturnValueParameters(IEnumerable values)
+ {
+ StaticMethodCalled(string.Join(',', values.Select(v => v.ToString())));
+ return true;
+ }
+
+
+ private void MethodCalled(string parameters = "", [CallerMemberName]string methodName = "")
+ {
+ this.methodName.Should().BeNull();
+ this.methodName = methodName;
+ this.parameters = parameters;
+
+ }
+
+
+ public static void ResetStatic()
+ {
+ staticMethodName = null;
+ staticParameters = null;
+ }
+
+
+ private static void StaticMethodCalled(string parameters = "", [CallerMemberName] string methodName = "")
+ {
+ staticMethodName.Should().BeNull();
+ staticMethodName = methodName;
+ staticParameters = parameters;
+ }
+
+
+
+ public void Verify(string methodName, string parameters = "")
+ {
+ this.methodName.Should().Be(methodName);
+ this.parameters.Should().Be(parameters);
+ }
+
+
+ public static void VerifyStatic(string methodName, string parameters = "")
+ {
+ staticMethodName.Should().Be(methodName);
+ staticParameters.Should().Be(parameters);
+ }
+ }
+ }
+}
diff --git a/Tapeti.Tests/Mock/MockDependencyResolver.cs b/Tapeti.Tests/Mock/MockDependencyResolver.cs
new file mode 100644
index 0000000..111ebac
--- /dev/null
+++ b/Tapeti.Tests/Mock/MockDependencyResolver.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace Tapeti.Tests.Mock
+{
+ public class MockDependencyResolver : IDependencyResolver
+ {
+ private readonly Dictionary container = new();
+
+
+ public void Set(TInterface instance) where TInterface : class
+ {
+ container.Add(typeof(TInterface), instance);
+ }
+
+
+ public T Resolve() where T : class
+ {
+ return (T)Resolve(typeof(T));
+ }
+
+
+ public object Resolve(Type type)
+ {
+ return container[type];
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tapeti.Tests/Mock/MockLogger.cs b/Tapeti.Tests/Mock/MockLogger.cs
new file mode 100644
index 0000000..bc02a26
--- /dev/null
+++ b/Tapeti.Tests/Mock/MockLogger.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Text;
+using Tapeti.Config;
+using Tapeti.Connection;
+using Xunit.Abstractions;
+
+namespace Tapeti.Tests.Mock
+{
+ internal class MockLogger : IBindingLogger
+ {
+ private readonly ITestOutputHelper testOutputHelper;
+
+
+ public MockLogger(ITestOutputHelper testOutputHelper)
+ {
+ this.testOutputHelper = testOutputHelper;
+ }
+
+
+ public void Connect(IConnectContext connectContext)
+ {
+ testOutputHelper.WriteLine($"{(connectContext.IsReconnect ? "Reconnecting" : "Connecting")} to {connectContext.ConnectionParams.HostName}:{connectContext.ConnectionParams.Port}{connectContext.ConnectionParams.VirtualHost}");
+ }
+
+ public void ConnectFailed(IConnectFailedContext connectContext)
+ {
+ testOutputHelper.WriteLine($"Connection failed: {connectContext.Exception}");
+ }
+
+ public void ConnectSuccess(IConnectSuccessContext connectContext)
+ {
+ testOutputHelper.WriteLine($"{(connectContext.IsReconnect ? "Reconnected" : "Connected")} using local port {connectContext.LocalPort}");
+ }
+
+ public void Disconnect(IDisconnectContext disconnectContext)
+ {
+ testOutputHelper.WriteLine($"Connection closed: {(!string.IsNullOrEmpty(disconnectContext.ReplyText) ? disconnectContext.ReplyText : "")} (reply code: {disconnectContext.ReplyCode})");
+ }
+
+ public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
+ {
+ testOutputHelper.WriteLine(exception.Message);
+ }
+
+ public void QueueDeclare(string queueName, bool durable, bool passive)
+ {
+ testOutputHelper.WriteLine(passive
+ ? $"Verifying durable queue {queueName}"
+ : $"Declaring {(durable ? "durable" : "dynamic")} queue {queueName}");
+ }
+
+ public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments)
+ {
+ testOutputHelper.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(IRabbitMQArguments? arguments)
+ {
+ if (arguments == null || arguments.Count == 0)
+ return "empty";
+
+ var argumentsText = new StringBuilder();
+ foreach (var pair in arguments)
+ {
+ if (argumentsText.Length > 0)
+ argumentsText.Append(", ");
+
+ argumentsText.Append($"{pair.Key} = {pair.Value}");
+ }
+
+ return argumentsText.ToString();
+ }
+
+
+ public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
+ {
+ testOutputHelper.WriteLine($"Binding {queueName} to exchange {exchange} with routing key {routingKey}");
+ }
+
+ public void QueueUnbind(string queueName, string exchange, string routingKey)
+ {
+ testOutputHelper.WriteLine($"Removing binding for {queueName} to exchange {exchange} with routing key {routingKey}");
+ }
+
+ public void ExchangeDeclare(string exchange)
+ {
+ testOutputHelper.WriteLine($"Declaring exchange {exchange}");
+ }
+
+ public void QueueObsolete(string queueName, bool deleted, uint messageCount)
+ {
+ testOutputHelper.WriteLine(deleted
+ ? $"Obsolete queue was deleted: {queueName}"
+ : $"Obsolete queue bindings removed: {queueName}, {messageCount} messages remaining");
+ }
+ }
+}
diff --git a/Tapeti.Tests/Tapeti.Tests.csproj b/Tapeti.Tests/Tapeti.Tests.csproj
index 4a6bfa1..a5d8368 100644
--- a/Tapeti.Tests/Tapeti.Tests.csproj
+++ b/Tapeti.Tests/Tapeti.Tests.csproj
@@ -1,7 +1,8 @@
- net5.0
+ net6.0;net7.0
+ enable
@@ -9,10 +10,13 @@
-
-
-
-
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers
@@ -22,8 +26,4 @@
-
-
-
-
diff --git a/Tapeti.Transient/ConfigExtensions.cs b/Tapeti.Transient/ConfigExtensions.cs
index aba7641..43a069c 100644
--- a/Tapeti.Transient/ConfigExtensions.cs
+++ b/Tapeti.Transient/ConfigExtensions.cs
@@ -1,6 +1,8 @@
using System;
using Tapeti.Config;
+// ReSharper disable UnusedMember.Global
+
namespace Tapeti.Transient
{
///
diff --git a/Tapeti.Transient/ITransientPublisher.cs b/Tapeti.Transient/ITransientPublisher.cs
index 7c01409..9aea93d 100644
--- a/Tapeti.Transient/ITransientPublisher.cs
+++ b/Tapeti.Transient/ITransientPublisher.cs
@@ -15,6 +15,6 @@ namespace Tapeti.Transient
///
///
///
- Task RequestResponse(TRequest request);
+ Task RequestResponse(TRequest request) where TRequest : class where TResponse : class;
}
}
\ No newline at end of file
diff --git a/Tapeti.Transient/Tapeti.Transient.csproj b/Tapeti.Transient/Tapeti.Transient.csproj
index 90f345a..27af569 100644
--- a/Tapeti.Transient/Tapeti.Transient.csproj
+++ b/Tapeti.Transient/Tapeti.Transient.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ net6.0;net7.0
true
Menno van Lavieren, Mark van Renswoude
@@ -11,6 +11,8 @@
https://github.com/MvRens/Tapeti
Tapeti.Flow.png
2.0.0
+ 9
+ enable
@@ -29,6 +31,6 @@
-
+
diff --git a/Tapeti.Transient/TransientExtension.cs b/Tapeti.Transient/TransientExtension.cs
index ec642e5..4b8d76b 100644
--- a/Tapeti.Transient/TransientExtension.cs
+++ b/Tapeti.Transient/TransientExtension.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Tapeti.Config;
namespace Tapeti.Transient
@@ -31,7 +32,7 @@ namespace Tapeti.Transient
///
public IEnumerable GetMiddleware(IDependencyResolver dependencyResolver)
{
- return null;
+ return Enumerable.Empty();
}
diff --git a/Tapeti.Transient/TransientGenericBinding.cs b/Tapeti.Transient/TransientGenericBinding.cs
index f55bb45..10c633f 100644
--- a/Tapeti.Transient/TransientGenericBinding.cs
+++ b/Tapeti.Transient/TransientGenericBinding.cs
@@ -4,7 +4,6 @@ using Tapeti.Config;
namespace Tapeti.Transient
{
- ///
///
/// Implements a binding for transient request response messages.
/// Register this binding using the WithTransient config extension method.
@@ -15,10 +14,10 @@ namespace Tapeti.Transient
private readonly string dynamicQueuePrefix;
///
- public string QueueName { get; private set; }
+ public string? QueueName { get; private set; }
///
- public QueueType QueueType => QueueType.Dynamic;
+ public QueueType? QueueType => Config.QueueType.Dynamic;
///
@@ -31,9 +30,9 @@ namespace Tapeti.Transient
///
- public async Task Apply(IBindingTarget target)
+ public async ValueTask Apply(IBindingTarget target)
{
- QueueName = await target.BindDynamicDirect(dynamicQueuePrefix);
+ QueueName = await target.BindDynamicDirect(dynamicQueuePrefix, null);
router.TransientResponseQueueName = QueueName;
}
@@ -46,17 +45,17 @@ namespace Tapeti.Transient
///
- public Task Invoke(IMessageContext context)
+ public ValueTask Invoke(IMessageContext context)
{
router.HandleMessage(context);
- return Task.CompletedTask;
+ return default;
}
///
- public Task Cleanup(IMessageContext context, ConsumeResult consumeResult)
+ public ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult)
{
- return Task.CompletedTask;
+ return default;
}
}
}
\ No newline at end of file
diff --git a/Tapeti.Transient/TransientPublisher.cs b/Tapeti.Transient/TransientPublisher.cs
index 3092c86..25880f7 100644
--- a/Tapeti.Transient/TransientPublisher.cs
+++ b/Tapeti.Transient/TransientPublisher.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Transient
{
- ///
///
/// Default implementation of ITransientPublisher
///
@@ -22,7 +21,7 @@ namespace Tapeti.Transient
///
- public async Task RequestResponse(TRequest request)
+ public async Task RequestResponse(TRequest request) where TRequest : class where TResponse : class
{
return (TResponse)await router.RequestResponse(publisher, request);
}
diff --git a/Tapeti.Transient/TransientRouter.cs b/Tapeti.Transient/TransientRouter.cs
index 2aeea29..5425e4c 100644
--- a/Tapeti.Transient/TransientRouter.cs
+++ b/Tapeti.Transient/TransientRouter.cs
@@ -13,12 +13,12 @@ namespace Tapeti.Transient
internal class TransientRouter
{
private readonly int defaultTimeoutMs;
- private readonly ConcurrentDictionary> map = new ConcurrentDictionary>();
+ private readonly ConcurrentDictionary> map = new();
///
/// The generated name of the dynamic queue to which responses should be sent.
///
- public string TransientResponseQueueName { get; set; }
+ public string? TransientResponseQueueName { get; set; }
///
@@ -41,8 +41,13 @@ namespace Tapeti.Transient
if (!Guid.TryParse(context.Properties.CorrelationId, out var continuationID))
return;
- if (map.TryRemove(continuationID, out var tcs))
- tcs.TrySetResult(context.Message);
+ if (!map.TryRemove(continuationID, out var tcs))
+ return;
+
+ if (context.Message == null)
+ throw new InvalidOperationException();
+
+ tcs.TrySetResult(context.Message);
}
@@ -55,7 +60,7 @@ namespace Tapeti.Transient
public async Task RequestResponse(IPublisher publisher, object request)
{
var correlation = Guid.NewGuid();
- var tcs = map.GetOrAdd(correlation, c => new TaskCompletionSource());
+ var tcs = map.GetOrAdd(correlation, _ => new TaskCompletionSource());
try
{
@@ -72,20 +77,22 @@ namespace Tapeti.Transient
{
// Simple cleanup of the task and map dictionary.
if (map.TryRemove(correlation, out tcs))
- tcs.TrySetResult(null);
+ tcs.TrySetResult(null!);
throw;
}
- using (new Timer(TimeoutResponse, tcs, defaultTimeoutMs, -1))
+ await using (new Timer(TimeoutResponse, tcs, defaultTimeoutMs, -1))
{
return await tcs.Task;
}
}
- private void TimeoutResponse(object tcs)
+ private void TimeoutResponse(object? tcs)
{
+ ArgumentNullException.ThrowIfNull(tcs, nameof(tcs));
+
((TaskCompletionSource)tcs).TrySetException(new TimeoutException("Transient RequestResponse timed out at (ms) " + defaultTimeoutMs));
}
}
diff --git a/Tapeti.UnityContainer/Tapeti.UnityContainer.csproj b/Tapeti.UnityContainer/Tapeti.UnityContainer.csproj
deleted file mode 100644
index bbd37c2..0000000
--- a/Tapeti.UnityContainer/Tapeti.UnityContainer.csproj
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
- netstandard2.0
- true
- Mark van Renswoude
-
- Unity container integration package for Tapeti
- rabbitmq tapeti unity
- Unlicense
- https://github.com/MvRens/Tapeti
- Tapeti.SimpleInjector.png
- 2.0.0
-
-
-
-
-
-
-
-
-
-
-
-
- True
-
-
-
-
-
-
-
-
diff --git a/Tapeti.UnityContainer/UnityDependencyResolver.cs b/Tapeti.UnityContainer/UnityDependencyResolver.cs
deleted file mode 100644
index a3ab84e..0000000
--- a/Tapeti.UnityContainer/UnityDependencyResolver.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using Unity;
-using Unity.Lifetime;
-
-namespace Tapeti.UnityContainer
-{
- ///
- ///
- /// Dependency resolver and container implementation for SimpleInjector.
- ///
- public class UnityDependencyResolver : IDependencyContainer
- {
- private readonly IUnityContainer container;
-
-
- ///
- ///
- public UnityDependencyResolver(IUnityContainer container)
- {
- this.container = container;
- }
-
-
- ///
- public T Resolve() where T : class
- {
- return container.Resolve();
- }
-
- ///
- public object Resolve(Type type)
- {
- return container.Resolve(type);
- }
-
-
- ///
- public void RegisterDefault() where TService : class where TImplementation : class, TService
- {
- if (container.IsRegistered(typeof(TService)))
- return;
-
- container.RegisterType();
- }
-
- ///
- public void RegisterDefault(Func factory) where TService : class
- {
- if (container.IsRegistered(typeof(TService)))
- return;
-
- container.RegisterFactory(c => factory());
- }
-
-
- ///
- public void RegisterDefaultSingleton() where TService : class where TImplementation : class, TService
- {
- if (container.IsRegistered(typeof(TService)))
- return;
-
- container.RegisterSingleton();
- }
-
- ///
- public void RegisterDefaultSingleton(TService instance) where TService : class
- {
- if (container.IsRegistered(typeof(TService)))
- return;
-
- container.RegisterInstance(instance);
- }
-
- ///
- public void RegisterDefaultSingleton(Func factory) where TService : class
- {
- if (container.IsRegistered(typeof(TService)))
- return;
-
- container.RegisterFactory(c => factory(), new SingletonLifetimeManager());
- }
-
-
- ///
- public void RegisterController(Type type)
- {
- container.RegisterType(type);
- }
- }
-}
diff --git a/Tapeti.sln b/Tapeti.sln
index d065fcf..d16ea52 100644
--- a/Tapeti.sln
+++ b/Tapeti.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31005.135
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}"
EndProject
@@ -45,8 +45,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.CastleWindsor", "Tap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Autofac", "Tapeti.Autofac\Tapeti.Autofac.csproj", "{B3802005-C941-41B6-A9A5-20573A7C24AE}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.UnityContainer", "Tapeti.UnityContainer\Tapeti.UnityContainer.csproj", "{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Ninject", "Tapeti.Ninject\Tapeti.Ninject.csproj", "{29478B10-FC53-4E93-ADEF-A775D9408131}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "06-StatelessRequestResponse", "Examples\06-StatelessRequestResponse\06-StatelessRequestResponse.csproj", "{152227AA-3165-4550-8997-6EA80C84516E}"
@@ -55,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "07-ParallelizationTest", "E
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "08-MessageHandlerLogging", "Examples\08-MessageHandlerLogging\08-MessageHandlerLogging.csproj", "{906605A6-2CAB-4B29-B0DD-B735BF265E39}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Benchmarks", "Tapeti.Benchmarks\Tapeti.Benchmarks.csproj", "{DBE56131-9207-4CEA-BA3E-031351677C48}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -129,10 +129,6 @@ Global
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.Build.0 = Release|Any CPU
- {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.Build.0 = Release|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -149,6 +145,10 @@ Global
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{906605A6-2CAB-4B29-B0DD-B735BF265E39}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DBE56131-9207-4CEA-BA3E-031351677C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DBE56131-9207-4CEA-BA3E-031351677C48}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DBE56131-9207-4CEA-BA3E-031351677C48}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DBE56131-9207-4CEA-BA3E-031351677C48}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -170,7 +170,6 @@ Global
{330D05CE-5321-4C7D-8017-2070B891289E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{374AAE64-598B-4F67-8870-4A05168FF987} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
{B3802005-C941-41B6-A9A5-20573A7C24AE} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
- {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
{29478B10-FC53-4E93-ADEF-A775D9408131} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
{152227AA-3165-4550-8997-6EA80C84516E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{E69E6BA5-68E7-4A4D-A38C-B2526AA66E96} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
diff --git a/Tapeti.sln.DotSettings b/Tapeti.sln.DotSettings
index 4a2b131..c44a322 100644
--- a/Tapeti.sln.DotSettings
+++ b/Tapeti.sln.DotSettings
@@ -4,7 +4,9 @@
ID
JSON
KV
+ MQ
SQL
+ UTF
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
True
True
diff --git a/Tapeti/Config/ControllerMessageContextPayload.cs b/Tapeti/Config/ControllerMessageContextPayload.cs
index 700d8a3..8bd3f0a 100644
--- a/Tapeti/Config/ControllerMessageContextPayload.cs
+++ b/Tapeti/Config/ControllerMessageContextPayload.cs
@@ -1,15 +1,14 @@
namespace Tapeti.Config
{
- ///
///
/// Extends the message context with information about the controller.
///
public class ControllerMessageContextPayload : IMessageContextPayload
{
///
- /// An instance of the controller referenced by the binding. Note: can be null during Cleanup.
+ /// An instance of the controller referenced by the binding. Note: can be null during Cleanup or when bound to static methods.
///
- public object Controller { get; }
+ public object? Controller { get; }
///
@@ -23,7 +22,7 @@
///
/// An instance of the controller referenced by the binding
/// The binding which is currently processing the message
- public ControllerMessageContextPayload(object controller, IControllerMethodBinding binding)
+ public ControllerMessageContextPayload(object? controller, IControllerMethodBinding binding)
{
Controller = controller;
Binding = binding;
diff --git a/Tapeti/Config/IBinding.cs b/Tapeti/Config/IBinding.cs
index 8cbb45f..d67255a 100644
--- a/Tapeti/Config/IBinding.cs
+++ b/Tapeti/Config/IBinding.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using Tapeti.Connection;
namespace Tapeti.Config
{
@@ -28,20 +29,20 @@ namespace Tapeti.Config
///
/// The name of the queue the binding is consuming. May change after a reconnect for dynamic queues.
///
- string QueueName { get; }
+ string? QueueName { get; }
///
/// Determines the type of queue the binding registers
///
- QueueType QueueType { get; }
+ QueueType? QueueType { get; }
///
/// Called after a connection is established to set up the binding.
///
///
- Task Apply(IBindingTarget target);
+ ValueTask Apply(IBindingTarget target);
///
@@ -55,7 +56,7 @@ namespace Tapeti.Config
/// Invokes the handler for the message as specified by the context.
///
///
- Task Invoke(IMessageContext context);
+ ValueTask Invoke(IMessageContext context);
///
@@ -64,7 +65,7 @@ namespace Tapeti.Config
///
///
///
- Task Cleanup(IMessageContext context, ConsumeResult consumeResult);
+ ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult);
}
@@ -80,7 +81,8 @@ namespace Tapeti.Config
///
/// The message class to be bound to the queue
/// The name of the durable queue
- Task BindDurable(Type messageClass, string queueName);
+ /// Optional arguments
+ ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments);
///
/// Binds the messageClass to a dynamic auto-delete queue.
@@ -91,15 +93,17 @@ namespace Tapeti.Config
///
/// The message class to be bound to the queue
/// 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.
+ /// Optional arguments
/// The generated name of the dynamic queue
- Task BindDynamic(Type messageClass, string queuePrefix = null);
+ ValueTask BindDynamic(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments);
///
/// Declares a durable queue but does not add a binding for a messageClass' routing key.
/// Used for direct-to-queue messages.
///
/// The name of the durable queue
- Task BindDurableDirect(string queueName);
+ /// Optional arguments
+ ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments);
///
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
@@ -107,22 +111,24 @@ namespace Tapeti.Config
///
/// The message class which will be handled on the queue. It is not actually bound to the queue.
/// 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.
+ /// Optional arguments
/// The generated name of the dynamic queue
- Task BindDynamicDirect(Type messageClass = null, string queuePrefix = null);
+ ValueTask BindDynamicDirect(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments);
///
/// 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.
///
/// 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.
+ /// Optional arguments
/// The generated name of the dynamic queue
- Task BindDynamicDirect(string queuePrefix = null);
+ ValueTask BindDynamicDirect(string? queuePrefix, IRabbitMQArguments? arguments);
///
/// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete
/// bindings and is empty, it will be removed.
///
/// The name of the durable queue
- Task BindDurableObsolete(string queueName);
+ ValueTask BindDurableObsolete(string queueName);
}
}
diff --git a/Tapeti/Config/IControllerBindingContext.cs b/Tapeti/Config/IControllerBindingContext.cs
index 37fb4d4..3d3e6d2 100644
--- a/Tapeti/Config/IControllerBindingContext.cs
+++ b/Tapeti/Config/IControllerBindingContext.cs
@@ -11,7 +11,7 @@ namespace Tapeti.Config
/// Injects a value for a controller method parameter.
///
///
- public delegate object ValueFactory(IMessageContext context);
+ public delegate object? ValueFactory(IMessageContext context);
///
@@ -19,7 +19,7 @@ namespace Tapeti.Config
///
///
///
- public delegate Task ResultHandler(IMessageContext context, object value);
+ public delegate ValueTask ResultHandler(IMessageContext context, object? value);
///
@@ -48,7 +48,7 @@ namespace Tapeti.Config
/// The message class for this method. Can be null if not yet set by the default MessageBinding or other middleware.
/// If required, call next first to ensure it is available.
///
- Type MessageClass { get; }
+ Type? MessageClass { get; }
///
/// Determines if SetMessageClass has already been called.
diff --git a/Tapeti/Config/IControllerBindingMiddleware.cs b/Tapeti/Config/IControllerBindingMiddleware.cs
index d88c951..4fdf341 100644
--- a/Tapeti/Config/IControllerBindingMiddleware.cs
+++ b/Tapeti/Config/IControllerBindingMiddleware.cs
@@ -2,18 +2,17 @@
namespace Tapeti.Config
{
- ///
///
/// Called when a Controller method is registered.
///
public interface IControllerBindingMiddleware : IControllerMiddlewareBase
{
- ///
- /// Called before a Controller method is registered. Can change the way parameters and return values are handled,
- /// and can inject message middleware specific to a method.
- ///
- ///
- /// Must be called to activate the new layer of middleware.
- void Handle(IControllerBindingContext context, Action next);
+ ///
+ /// Called before a Controller method is registered. Can change the way parameters and return values are handled,
+ /// and can inject message middleware specific to a method.
+ ///
+ ///
+ /// Must be called to activate the new layer of middleware.
+ void Handle(IControllerBindingContext context, Action next);
}
}
diff --git a/Tapeti/Config/IControllerCleanupMiddleware.cs b/Tapeti/Config/IControllerCleanupMiddleware.cs
index 86ef003..980c4a7 100644
--- a/Tapeti/Config/IControllerCleanupMiddleware.cs
+++ b/Tapeti/Config/IControllerCleanupMiddleware.cs
@@ -14,6 +14,6 @@ namespace Tapeti.Config
///
///
/// Always call to allow the next in the chain to clean up
- Task Cleanup(IMessageContext context, ConsumeResult consumeResult, Func next);
+ ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult, Func next);
}
}
diff --git a/Tapeti/Config/IControllerFilterMiddleware.cs b/Tapeti/Config/IControllerFilterMiddleware.cs
index 6a30e20..dc7be4e 100644
--- a/Tapeti/Config/IControllerFilterMiddleware.cs
+++ b/Tapeti/Config/IControllerFilterMiddleware.cs
@@ -3,7 +3,6 @@ using System.Threading.Tasks;
namespace Tapeti.Config
{
- ///
///
/// Denotes middleware that runs before the controller is instantiated.
///
@@ -15,6 +14,6 @@ namespace Tapeti.Config
///
///
///
- Task Filter(IMessageContext context, Func next);
+ ValueTask Filter(IMessageContext context, Func next);
}
}
diff --git a/Tapeti/Config/IControllerMessageMiddleware.cs b/Tapeti/Config/IControllerMessageMiddleware.cs
index c381270..e497ead 100644
--- a/Tapeti/Config/IControllerMessageMiddleware.cs
+++ b/Tapeti/Config/IControllerMessageMiddleware.cs
@@ -14,6 +14,6 @@ namespace Tapeti.Config
///
///
/// Call to pass the message to the next handler in the chain or call the controller method
- Task Handle(IMessageContext context, Func next);
+ ValueTask Handle(IMessageContext context, Func next);
}
}
diff --git a/Tapeti/Config/IControllerMethodBinding.cs b/Tapeti/Config/IControllerMethodBinding.cs
index 0fb4ce5..2d4c50f 100644
--- a/Tapeti/Config/IControllerMethodBinding.cs
+++ b/Tapeti/Config/IControllerMethodBinding.cs
@@ -3,7 +3,6 @@ using System.Reflection;
namespace Tapeti.Config
{
- ///
///
/// Represents a binding to a method in a controller class to handle incoming messages.
///
diff --git a/Tapeti/Config/IMessageContext.cs b/Tapeti/Config/IMessageContext.cs
index 6fea4cc..2d6c77e 100644
--- a/Tapeti/Config/IMessageContext.cs
+++ b/Tapeti/Config/IMessageContext.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
// ReSharper disable UnusedMemberInSuper.Global - public API
@@ -34,12 +35,12 @@ namespace Tapeti.Config
///
/// Contains the raw body of the message.
///
- byte[] RawBody { get; }
+ byte[]? RawBody { get; }
///
/// Contains the decoded message instance.
///
- object Message { get; }
+ object? Message { get; }
///
/// Provides access to the message metadata.
@@ -55,7 +56,7 @@ namespace Tapeti.Config
/// Contains a CancellationToken which is cancelled when the connection to the RabbitMQ server is closed.
/// Note that this token is cancelled regardless of whether the connection will be reestablished, as any
/// messages still in the queue will be redelivered with a new token.
- ///
+ ///
CancellationToken ConnectionClosed { get; }
///
@@ -87,7 +88,7 @@ namespace Tapeti.Config
/// Returns true and the payload value if this message context was previously enriched with the payload T.
///
/// The payload type as passed to Enrich
- bool TryGet(out T payload) where T : IMessageContextPayload;
+ bool TryGet([NotNullWhen(true)] out T? payload) where T : IMessageContextPayload;
///
/// Stores a key-value pair in the context for passing information between the various
@@ -105,7 +106,7 @@ namespace Tapeti.Config
///
/// True if the value was found, False otherwise
[Obsolete("For backwards compatibility only. Use Get payload overload for typed properties instead")]
- bool Get(string key, out T value) where T : class;
+ bool Get(string key, out T? value) where T : class;
}
diff --git a/Tapeti/Config/IMessageMiddleware.cs b/Tapeti/Config/IMessageMiddleware.cs
index 134b5de..228a1a6 100644
--- a/Tapeti/Config/IMessageMiddleware.cs
+++ b/Tapeti/Config/IMessageMiddleware.cs
@@ -13,6 +13,6 @@ namespace Tapeti.Config
///
///
/// Call to pass the message to the next handler in the chain
- Task Handle(IMessageContext context, Func next);
+ ValueTask Handle(IMessageContext context, Func next);
}
}
diff --git a/Tapeti/Config/IMessageProperties.cs b/Tapeti/Config/IMessageProperties.cs
index 31c203b..07e7eaf 100644
--- a/Tapeti/Config/IMessageProperties.cs
+++ b/Tapeti/Config/IMessageProperties.cs
@@ -9,13 +9,13 @@ namespace Tapeti.Config
public interface IMessageProperties
{
///
- string ContentType { get; set; }
+ string? ContentType { get; set; }
///
- string CorrelationId { get; set; }
+ string? CorrelationId { get; set; }
///
- string ReplyTo { get; set; }
+ string? ReplyTo { get; set; }
///
bool? Persistent { get; set; }
@@ -37,7 +37,7 @@ namespace Tapeti.Config
///
///
/// The value if found, null otherwise
- string GetHeader(string name);
+ string? GetHeader(string name);
///
diff --git a/Tapeti/Config/IPublishContext.cs b/Tapeti/Config/IPublishContext.cs
index a5fb435..60c0859 100644
--- a/Tapeti/Config/IPublishContext.cs
+++ b/Tapeti/Config/IPublishContext.cs
@@ -16,7 +16,7 @@ namespace Tapeti.Config
///
/// The exchange to which the message will be published.
///
- string Exchange { get; set; }
+ string? Exchange { get; set; }
///
/// The routing key which will be included with the message.
@@ -31,6 +31,6 @@ namespace Tapeti.Config
///
/// Provides access to the message metadata.
///
- IMessageProperties Properties { get; }
+ IMessageProperties? Properties { get; }
}
}
diff --git a/Tapeti/Config/IPublishMiddleware.cs b/Tapeti/Config/IPublishMiddleware.cs
index c8069e3..e68cf88 100644
--- a/Tapeti/Config/IPublishMiddleware.cs
+++ b/Tapeti/Config/IPublishMiddleware.cs
@@ -13,6 +13,6 @@ namespace Tapeti.Config
///
///
/// Call to pass the message to the next handler in the chain
- Task Handle(IPublishContext context, Func next);
+ ValueTask Handle(IPublishContext context, Func next);
}
}
diff --git a/Tapeti/Config/ITapetiConfig.cs b/Tapeti/Config/ITapetiConfig.cs
index 519eeb2..4bdad56 100644
--- a/Tapeti/Config/ITapetiConfig.cs
+++ b/Tapeti/Config/ITapetiConfig.cs
@@ -77,7 +77,6 @@ namespace Tapeti.Config
}
- ///
///
/// Contains a list of registered bindings, with a few added helpers.
///
@@ -88,40 +87,13 @@ namespace Tapeti.Config
///
///
/// The binding if found, null otherwise
- IControllerMethodBinding ForMethod(Delegate method);
+ IControllerMethodBinding? ForMethod(Delegate method);
///
/// Searches for a binding linked to the specified method.
///
///
/// The binding if found, null otherwise
- IControllerMethodBinding ForMethod(MethodInfo method);
+ IControllerMethodBinding? ForMethod(MethodInfo method);
}
-
-
- /*
- public interface IBinding
- {
- Type Controller { get; }
- MethodInfo Method { get; }
- Type MessageClass { get; }
- string QueueName { get; }
- QueueBindingMode QueueBindingMode { get; set; }
-
- IReadOnlyList MessageFilterMiddleware { get; }
- IReadOnlyList MessageMiddleware { get; }
-
- bool Accept(Type messageClass);
- bool Accept(IMessageContext context, object message);
- Task Invoke(IMessageContext context, object message);
- }
- */
-
-
- /*
- public interface IBuildBinding : IBinding
- {
- void SetQueueName(string queueName);
- }
- */
}
diff --git a/Tapeti/Config/ITapetiConfigBuilder.cs b/Tapeti/Config/ITapetiConfigBuilder.cs
index 87699df..4fc1713 100644
--- a/Tapeti/Config/ITapetiConfigBuilder.cs
+++ b/Tapeti/Config/ITapetiConfigBuilder.cs
@@ -118,7 +118,7 @@ namespace Tapeti.Config
/// before the configuration is built. Implementations of ITapetiConfigBuilder should also implement this interface.
/// Should not be used outside of Tapeti packages.
///
- public interface ITapetiConfigBuilderAccess
+ public interface ITapetiConfigBuilderAccess : ITapetiConfigBuilder
{
///
/// Provides access to the dependency resolver.
diff --git a/Tapeti/Config/ITapetiExtensionBinding.cs b/Tapeti/Config/ITapetiExtensionBinding.cs
index 33b064e..cfcbc30 100644
--- a/Tapeti/Config/ITapetiExtensionBinding.cs
+++ b/Tapeti/Config/ITapetiExtensionBinding.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Config
{
- ///
///
/// Provides a way for Tapeti extensions to register custom bindings.
///
diff --git a/Tapeti/Connection/ITapetiClient.cs b/Tapeti/Connection/ITapetiClient.cs
index e71c3fc..eeda4f3 100644
--- a/Tapeti/Connection/ITapetiClient.cs
+++ b/Tapeti/Connection/ITapetiClient.cs
@@ -6,7 +6,6 @@ using Tapeti.Config;
namespace Tapeti.Connection
{
- ///
///
/// Defines a queue binding to an exchange using a routing key
///
@@ -38,9 +37,9 @@ namespace Tapeti.Connection
}
///
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
- if (ReferenceEquals(null, obj)) return false;
+ if (obj is null) return false;
return obj is QueueBinding other && Equals(other);
}
@@ -52,6 +51,18 @@ namespace Tapeti.Connection
return ((Exchange != null ? Exchange.GetHashCode() : 0) * 397) ^ (RoutingKey != null ? RoutingKey.GetHashCode() : 0);
}
}
+
+ ///
+ public static bool operator ==(QueueBinding left, QueueBinding right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ public static bool operator !=(QueueBinding left, QueueBinding right)
+ {
+ return !left.Equals(right);
+ }
}
@@ -68,17 +79,17 @@ namespace Tapeti.Connection
/// The exchange to publish the message to, or empty to send it directly to a queue
/// The routing key for the message, or queue name if exchange is empty
/// If true, an exception will be raised if the message can not be delivered to at least one queue
- Task Publish(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory);
+ Task Publish(byte[] body, IMessageProperties properties, string? exchange, string routingKey, bool mandatory);
///
/// Starts a consumer for the specified queue, using the provided bindings to handle messages.
///
- /// Cancelled when the connection is lost
///
/// The consumer implementation which will receive the messages from the queue
+ /// Cancelled when the connection is lost
/// The consumer tag as returned by BasicConsume.
- Task Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer);
+ Task Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken);
///
/// Stops the consumer with the specified tag.
@@ -89,40 +100,43 @@ namespace Tapeti.Connection
///
/// Creates a durable queue if it does not already exist, and updates the bindings.
///
- /// Cancelled when the connection is lost
/// The name of the queue to create
/// A list of bindings. Any bindings already on the queue which are not in this list will be removed
- Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable bindings);
+ /// Optional arguments
+ /// Cancelled when the connection is lost
+ Task DurableQueueDeclare(string queueName, IEnumerable bindings, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
///
/// Verifies a durable queue exists. Will raise an exception if it does not.
///
- /// Cancelled when the connection is lost
/// The name of the queue to verify
- Task DurableQueueVerify(CancellationToken cancellationToken, string queueName);
+ /// Optional arguments
+ /// Cancelled when the connection is lost
+ Task DurableQueueVerify(string queueName, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
///
/// Deletes a durable queue.
///
- /// Cancelled when the connection is lost
/// The name of the queue to delete
/// 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.
- Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true);
+ /// Cancelled when the connection is lost
+ Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken);
///
/// Creates a dynamic queue.
///
- /// Cancelled when the connection is lost
/// 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.
- Task DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null);
+ /// Optional arguments
+ /// Cancelled when the connection is lost
+ Task DynamicQueueDeclare(string? queuePrefix, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
///
/// Add a binding to a dynamic queue.
///
- /// Cancelled when the connection is lost
/// The name of the dynamic queue previously created using DynamicQueueDeclare
/// The binding to add to the dynamic queue
- Task DynamicQueueBind(CancellationToken cancellationToken, string queueName, QueueBinding binding);
+ /// Cancelled when the connection is lost
+ Task DynamicQueueBind(string queueName, QueueBinding binding, CancellationToken cancellationToken);
///
/// Closes the connection to RabbitMQ gracefully.
diff --git a/Tapeti/Connection/RabbitMQArguments.cs b/Tapeti/Connection/RabbitMQArguments.cs
new file mode 100644
index 0000000..9e0a576
--- /dev/null
+++ b/Tapeti/Connection/RabbitMQArguments.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace Tapeti.Connection
+{
+ ///
+ public interface IRabbitMQArguments : IReadOnlyDictionary
+ {
+ }
+
+
+ internal class RabbitMQArguments : Dictionary, IRabbitMQArguments
+ {
+ public RabbitMQArguments()
+ {
+ }
+
+
+ public RabbitMQArguments(IReadOnlyDictionary values) : base(values)
+ {
+ }
+
+
+ public void AddUTF8(string key, string value)
+ {
+ Add(key, Encoding.UTF8.GetBytes(value));
+ }
+ }
+}
diff --git a/Tapeti/Connection/TapetiBasicConsumer.cs b/Tapeti/Connection/TapetiBasicConsumer.cs
index b465ee2..5b04ba5 100644
--- a/Tapeti/Connection/TapetiBasicConsumer.cs
+++ b/Tapeti/Connection/TapetiBasicConsumer.cs
@@ -14,7 +14,6 @@ namespace Tapeti.Connection
public delegate Task ResponseFunc(long expectedConnectionReference, ulong deliveryTag, ConsumeResult result);
- ///
///
/// Implements the bridge between the RabbitMQ Client consumer and a Tapeti Consumer
///
diff --git a/Tapeti/Connection/TapetiChannel.cs b/Tapeti/Connection/TapetiChannel.cs
index 0efe5c3..ed39446 100644
--- a/Tapeti/Connection/TapetiChannel.cs
+++ b/Tapeti/Connection/TapetiChannel.cs
@@ -21,7 +21,7 @@ namespace Tapeti.Connection
{
private readonly Func modelFactory;
private readonly object taskQueueLock = new();
- private SingleThreadTaskQueue taskQueue;
+ private SingleThreadTaskQueue? taskQueue;
private readonly ModelProvider modelProvider;
@@ -34,7 +34,7 @@ namespace Tapeti.Connection
public async Task Reset()
{
- SingleThreadTaskQueue capturedTaskQueue;
+ SingleThreadTaskQueue? capturedTaskQueue;
lock (taskQueueLock)
{
diff --git a/Tapeti/Connection/TapetiClient.cs b/Tapeti/Connection/TapetiClient.cs
index 17ccd30..ec1316b 100644
--- a/Tapeti/Connection/TapetiClient.cs
+++ b/Tapeti/Connection/TapetiClient.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -7,12 +7,14 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using RabbitMQ.Client.Exceptions;
using Tapeti.Config;
using Tapeti.Default;
using Tapeti.Exceptions;
+using Tapeti.Helpers;
namespace Tapeti.Connection
{
@@ -23,7 +25,6 @@ namespace Tapeti.Connection
}
- ///
///
/// Implementation of ITapetiClient for the RabbitMQ Client library
///
@@ -42,7 +43,7 @@ namespace Tapeti.Connection
///
/// Receives events when the connection state changes.
///
- public IConnectionEventListener ConnectionEventListener { get; set; }
+ public IConnectionEventListener? ConnectionEventListener { get; set; }
private readonly TapetiChannel consumeChannel;
@@ -52,9 +53,9 @@ namespace Tapeti.Connection
// These fields must be locked using connectionLock
private readonly object connectionLock = new();
private long connectionReference;
- private RabbitMQ.Client.IConnection connection;
- private IModel consumeChannelModel;
- private IModel publishChannelModel;
+ private RabbitMQ.Client.IConnection? connection;
+ private IModel? consumeChannelModel;
+ private IModel? publishChannelModel;
private bool isClosing;
private bool isReconnect;
private DateTime connectedDateTime;
@@ -71,8 +72,15 @@ namespace Tapeti.Connection
private class ConfirmMessageInfo
{
- public string ReturnKey;
- public TaskCompletionSource CompletionSource;
+ public string ReturnKey { get; }
+ public TaskCompletionSource CompletionSource { get; }
+
+
+ public ConfirmMessageInfo(string returnKey, TaskCompletionSource completionSource)
+ {
+ ReturnKey = returnKey;
+ CompletionSource = completionSource;
+ }
}
@@ -109,7 +117,7 @@ namespace Tapeti.Connection
///
- public async Task Publish(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory)
+ public async Task Publish(byte[] body, IMessageProperties properties, string? exchange, string routingKey, bool mandatory)
{
if (string.IsNullOrEmpty(routingKey))
throw new ArgumentNullException(nameof(routingKey));
@@ -117,17 +125,14 @@ namespace Tapeti.Connection
await GetTapetiChannel(TapetiChannelType.Publish).QueueWithProvider(async channelProvider =>
{
- Task publishResultTask = null;
- var messageInfo = new ConfirmMessageInfo
- {
- ReturnKey = GetReturnKey(exchange, routingKey),
- CompletionSource = new TaskCompletionSource()
- };
+ Task? publishResultTask = null;
+ var messageInfo = new ConfirmMessageInfo(GetReturnKey(exchange ?? string.Empty, routingKey), new TaskCompletionSource());
channelProvider.WithRetryableChannel(channel =>
{
- DeclareExchange(channel, exchange);
+ if (exchange != null)
+ DeclareExchange(channel, exchange);
// The delivery tag is lost after a reconnect, register under the new tag
if (config.Features.PublisherConfirms)
@@ -152,7 +157,7 @@ namespace Tapeti.Connection
try
{
var publishProperties = new RabbitMQMessageProperties(channel.CreateBasicProperties(), properties);
- channel.BasicPublish(exchange ?? "", routingKey, mandatory, publishProperties.BasicProperties, body);
+ channel.BasicPublish(exchange ?? string.Empty, routingKey, mandatory, publishProperties.BasicProperties, body);
}
catch
{
@@ -191,7 +196,7 @@ namespace Tapeti.Connection
case 312:
throw new NoRouteException(
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' does not have a route");
-
+
case > 0:
throw new NoRouteException(
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' could not be delivered, reply code: {replyCode}");
@@ -201,7 +206,7 @@ namespace Tapeti.Connection
///
- public async Task Consume(CancellationToken cancellationToken, string queueName, IConsumer consumer)
+ public async Task Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken)
{
if (deletedQueues.Contains(queueName))
return null;
@@ -211,7 +216,7 @@ namespace Tapeti.Connection
long capturedConnectionReference = -1;
- string consumerTag = null;
+ string? consumerTag = null;
await GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(channel =>
{
@@ -223,7 +228,9 @@ namespace Tapeti.Connection
consumerTag = channel.BasicConsume(queueName, false, basicConsumer);
});
- return new TapetiConsumerTag(capturedConnectionReference, consumerTag);
+ return consumerTag == null
+ ? null
+ : new TapetiConsumerTag(capturedConnectionReference, consumerTag);
}
@@ -288,7 +295,7 @@ namespace Tapeti.Connection
}
- private async Task GetDurableQueueDeclareRequired(string queueName)
+ private async Task GetDurableQueueDeclareRequired(string queueName, IRabbitMQArguments? arguments)
{
var existingQueue = await GetQueueInfo(queueName);
if (existingQueue == null)
@@ -297,18 +304,45 @@ namespace Tapeti.Connection
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)");
- if (existingQueue.Arguments.Count <= 0)
+ var existingArguments = ConvertJsonArguments(existingQueue.Arguments);
+ if (existingArguments.NullSafeSameValues(arguments))
return true;
-
- (logger as IBindingLogger)?.QueueExistsWarning(queueName, existingQueue.Arguments);
+
+ (logger as IBindingLogger)?.QueueExistsWarning(queueName, existingArguments, arguments);
return false;
}
-
+
+
+ private static RabbitMQArguments? ConvertJsonArguments(IReadOnlyDictionary? arguments)
+ {
+ if (arguments == null)
+ return null;
+
+ var result = new RabbitMQArguments();
+ foreach (var pair in arguments)
+ {
+ // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault - by design
+ object value = pair.Value.Type switch
+ {
+ JTokenType.Integer => pair.Value.Value(),
+ JTokenType.Float => pair.Value.Value(),
+ JTokenType.String => Encoding.UTF8.GetBytes(pair.Value.Value() ?? string.Empty),
+ JTokenType.Boolean => pair.Value.Value(),
+ _ => throw new ArgumentOutOfRangeException(nameof(arguments))
+ };
+
+ result.Add(pair.Key, value);
+ }
+
+ return result;
+ }
+
+
///
- public async Task DurableQueueDeclare(CancellationToken cancellationToken, string queueName, IEnumerable bindings)
+ public async Task DurableQueueDeclare(string queueName, IEnumerable bindings, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
{
- var declareRequired = await GetDurableQueueDeclareRequired(queueName);
+ var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments);
var existingBindings = (await GetQueueBindings(queueName)).ToList();
var currentBindings = bindings.ToList();
@@ -322,7 +356,7 @@ namespace Tapeti.Connection
if (declareRequired)
{
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))
@@ -340,10 +374,19 @@ namespace Tapeti.Connection
});
}
- ///
- public async Task DurableQueueVerify(CancellationToken cancellationToken, string queueName)
+
+ private static IDictionary? GetDeclareArguments(IRabbitMQArguments? arguments)
{
- if (!await GetDurableQueueDeclareRequired(queueName))
+ return arguments == null || arguments.Count == 0
+ ? null
+ : arguments.ToDictionary(p => p.Key, p => p.Value);
+ }
+
+
+ ///
+ public async Task DurableQueueVerify(string queueName, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
+ {
+ if (!await GetDurableQueueDeclareRequired(queueName, arguments))
return;
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
@@ -358,7 +401,7 @@ namespace Tapeti.Connection
///
- public async Task DurableQueueDelete(CancellationToken cancellationToken, string queueName, bool onlyIfEmpty = true)
+ public async Task DurableQueueDelete(string queueName, bool onlyIfEmpty, CancellationToken cancellationToken)
{
if (!onlyIfEmpty)
{
@@ -443,9 +486,9 @@ namespace Tapeti.Connection
///
- public async Task DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null)
+ public async Task DynamicQueueDeclare(string? queuePrefix, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
{
- string queueName = null;
+ string? queueName = null;
var bindingLogger = logger as IBindingLogger;
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
@@ -457,20 +500,24 @@ namespace Tapeti.Connection
{
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
bindingLogger?.QueueDeclare(queueName, false, false);
- channel.QueueDeclare(queueName);
+ channel.QueueDeclare(queueName, arguments: GetDeclareArguments(arguments));
}
else
{
- queueName = channel.QueueDeclare().QueueName;
+ queueName = channel.QueueDeclare(arguments: GetDeclareArguments(arguments)).QueueName;
bindingLogger?.QueueDeclare(queueName, false, false);
}
});
+ cancellationToken.ThrowIfCancellationRequested();
+ if (queueName == null)
+ throw new InvalidOperationException("Failed to declare dynamic queue");
+
return queueName;
}
///
- 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 =>
{
@@ -487,9 +534,9 @@ namespace Tapeti.Connection
///
public async Task Close()
{
- IModel capturedConsumeModel;
- IModel capturedPublishModel;
- RabbitMQ.Client.IConnection capturedConnection;
+ IModel? capturedConsumeModel;
+ IModel? capturedPublishModel;
+ RabbitMQ.Client.IConnection? capturedConnection;
lock (connectionLock)
{
@@ -537,10 +584,10 @@ namespace Tapeti.Connection
private class ManagementQueueInfo
{
[JsonProperty("name")]
- public string Name { get; set; }
+ public string? Name { get; set; }
[JsonProperty("vhost")]
- public string VHost { get; set; }
+ public string? VHost { get; set; }
[JsonProperty("durable")]
public bool Durable { get; set; }
@@ -552,7 +599,7 @@ namespace Tapeti.Connection
public bool Exclusive { get; set; }
[JsonProperty("arguments")]
- public Dictionary Arguments { get; set; }
+ public Dictionary? Arguments { get; set; }
[JsonProperty("messages")]
public uint Messages { get; set; }
@@ -560,7 +607,7 @@ namespace Tapeti.Connection
- private async Task GetQueueInfo(string queueName)
+ private async Task GetQueueInfo(string queueName)
{
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
var queuePath = Uri.EscapeDataString(queueName);
@@ -581,25 +628,25 @@ namespace Tapeti.Connection
private class ManagementBinding
{
[JsonProperty("source")]
- public string Source { get; set; }
+ public string? Source { get; set; }
[JsonProperty("vhost")]
- public string Vhost { get; set; }
+ public string? Vhost { get; set; }
[JsonProperty("destination")]
- public string Destination { get; set; }
+ public string? Destination { get; set; }
[JsonProperty("destination_type")]
- public string DestinationType { get; set; }
+ public string? DestinationType { get; set; }
[JsonProperty("routing_key")]
- public string RoutingKey { get; set; }
+ public string? RoutingKey { get; set; }
[JsonProperty("arguments")]
- public Dictionary Arguments { get; set; }
+ public Dictionary? Arguments { get; set; }
[JsonProperty("properties_key")]
- public string PropertiesKey { get; set; }
+ public string? PropertiesKey { get; set; }
}
@@ -617,8 +664,8 @@ namespace Tapeti.Connection
// Filter out the binding to an empty source, which is always present for direct-to-queue routing
return bindings?
- .Where(binding => !string.IsNullOrEmpty(binding.Source))
- .Select(binding => new QueueBinding(binding.Source, binding.RoutingKey))
+ .Where(binding => !string.IsNullOrEmpty(binding.Source) && !string.IsNullOrEmpty(binding.RoutingKey))
+ .Select(binding => new QueueBinding(binding.Source!, binding.RoutingKey!))
?? Enumerable.Empty();
});
}
@@ -663,7 +710,7 @@ namespace Tapeti.Connection
}
catch (WebException e)
{
- if (!(e.Response is HttpWebResponse response))
+ if (e.Response is not HttpWebResponse response)
throw;
if (!TransientStatusCodes.Contains(response.StatusCode))
@@ -682,9 +729,6 @@ namespace Tapeti.Connection
private void DeclareExchange(IModel channel, string exchange)
{
- if (string.IsNullOrEmpty(exchange))
- return;
-
if (declaredExchanges.Contains(exchange))
return;
@@ -714,7 +758,7 @@ namespace Tapeti.Connection
? publishChannelModel
: consumeChannelModel;
- if (channel != null && channel.IsOpen)
+ if (channel is { IsOpen: true })
return channel;
}
@@ -750,9 +794,9 @@ namespace Tapeti.Connection
{
try
{
- RabbitMQ.Client.IConnection capturedConnection;
- IModel capturedConsumeChannelModel;
- IModel capturedPublishChannelModel;
+ RabbitMQ.Client.IConnection? capturedConnection;
+ IModel? capturedConsumeChannelModel;
+ IModel? capturedPublishChannelModel;
lock (connectionLock)
@@ -764,7 +808,7 @@ namespace Tapeti.Connection
{
try
{
- if (connection.IsOpen)
+ if (connection is { IsOpen: true })
connection.Close();
}
catch (AlreadyClosedException)
@@ -772,7 +816,7 @@ namespace Tapeti.Connection
}
finally
{
- connection.Dispose();
+ connection?.Dispose();
}
connection = null;
@@ -832,12 +876,7 @@ namespace Tapeti.Connection
consumeChannelModel = null;
}
- ConnectionEventListener?.Disconnected(new DisconnectedEventArgs
- {
- ReplyCode = e.ReplyCode,
- ReplyText = e.ReplyText
- });
-
+ ConnectionEventListener?.Disconnected(new DisconnectedEventArgs(e.ReplyCode, e.ReplyText));
logger.Disconnect(new DisconnectContext(connectionParams, e.ReplyCode, e.ReplyText));
// Reconnect if the disconnect was unexpected
@@ -865,11 +904,7 @@ namespace Tapeti.Connection
connectedDateTime = DateTime.UtcNow;
- var connectedEventArgs = new ConnectedEventArgs
- {
- ConnectionParams = connectionParams,
- LocalPort = capturedConnection.LocalPort
- };
+ var connectedEventArgs = new ConnectedEventArgs(connectionParams, capturedConnection.LocalPort);
if (isReconnect)
ConnectionEventListener?.Reconnected(connectedEventArgs);
@@ -897,7 +932,7 @@ namespace Tapeti.Connection
}
- private void HandleBasicReturn(object sender, BasicReturnEventArgs e)
+ private void HandleBasicReturn(object? sender, BasicReturnEventArgs e)
{
/*
* "If the message is also published as mandatory, the basic.return is sent to the client before basic.ack."
@@ -927,7 +962,7 @@ namespace Tapeti.Connection
}
- private void HandleBasicAck(object sender, BasicAckEventArgs e)
+ private void HandleBasicAck(object? sender, BasicAckEventArgs e)
{
Monitor.Enter(confirmLock);
try
@@ -958,7 +993,7 @@ namespace Tapeti.Connection
}
- private void HandleBasicNack(object sender, BasicNackEventArgs e)
+ private void HandleBasicNack(object? sender, BasicNackEventArgs e)
{
Monitor.Enter(confirmLock);
try
@@ -1007,10 +1042,10 @@ namespace Tapeti.Connection
public TapetiConnectionParams ConnectionParams { get; }
public bool IsReconnect { get; }
public int LocalPort { get; }
- public Exception Exception { get; }
+ public Exception? Exception { get; }
- public ConnectContext(TapetiConnectionParams connectionParams, bool isReconnect, int localPort = 0, Exception exception = null)
+ public ConnectContext(TapetiConnectionParams connectionParams, bool isReconnect, int localPort = 0, Exception? exception = null)
{
ConnectionParams = connectionParams;
IsReconnect = isReconnect;
diff --git a/Tapeti/Connection/TapetiConsumer.cs b/Tapeti/Connection/TapetiConsumer.cs
index fba63dd..fb0ddb8 100644
--- a/Tapeti/Connection/TapetiConsumer.cs
+++ b/Tapeti/Connection/TapetiConsumer.cs
@@ -9,7 +9,6 @@ using Tapeti.Helpers;
namespace Tapeti.Connection
{
- ///
///
/// Implements a RabbitMQ consumer to pass messages to the Tapeti middleware.
///
@@ -41,7 +40,7 @@ namespace Tapeti.Connection
///
public async Task Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
{
- object message = null;
+ object? message = null;
try
{
try
@@ -74,7 +73,7 @@ namespace Tapeti.Connection
RawBody = body,
Message = message,
Properties = properties,
- Binding = null,
+ Binding = new ExceptionContextBinding(queueName),
ConnectionClosed = CancellationToken.None
};
@@ -172,7 +171,7 @@ namespace Tapeti.Connection
return e switch
{
AggregateException aggregateException => aggregateException.InnerExceptions.Any(IgnoreExceptionDuringShutdown),
- TaskCanceledException or OperationCanceledException => true,
+ OperationCanceledException => true,
_ => e.InnerException != null && IgnoreExceptionDuringShutdown(e.InnerException)
};
}
@@ -185,5 +184,42 @@ namespace Tapeti.Connection
public string RoutingKey;
public IMessageProperties Properties;
}
+
+
+ private class ExceptionContextBinding : IBinding
+ {
+ public string? QueueName { get; }
+ public QueueType? QueueType => null;
+
+
+ public ExceptionContextBinding(string? queueName)
+ {
+ QueueName = queueName;
+ }
+
+
+ public ValueTask Apply(IBindingTarget target)
+ {
+ throw new InvalidOperationException("Apply method should not be called on a binding in an Exception context");
+ }
+
+
+ public bool Accept(Type messageClass)
+ {
+ throw new InvalidOperationException("Accept method should not be called on a binding in an Exception context");
+ }
+
+
+ public ValueTask Invoke(IMessageContext context)
+ {
+ throw new InvalidOperationException("Invoke method should not be called on a binding in an Exception context");
+ }
+
+
+ public ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult)
+ {
+ throw new InvalidOperationException("Cleanup method should not be called on a binding in an Exception context");
+ }
+ }
}
}
diff --git a/Tapeti/Connection/TapetiPublisher.cs b/Tapeti/Connection/TapetiPublisher.cs
index c590528..8f18b10 100644
--- a/Tapeti/Connection/TapetiPublisher.cs
+++ b/Tapeti/Connection/TapetiPublisher.cs
@@ -38,14 +38,14 @@ namespace Tapeti.Connection
///
- public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class
+ public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class
{
await PublishRequest(message, responseMethodSelector.Body);
}
///
- public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class
+ public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class
{
await PublishRequest(message, responseMethodSelector.Body);
}
@@ -97,7 +97,7 @@ namespace Tapeti.Connection
///
- public async Task Publish(object message, IMessageProperties properties, bool mandatory)
+ public async Task Publish(object message, IMessageProperties? properties, bool mandatory)
{
var messageClass = message.GetType();
var exchange = exchangeStrategy.GetExchange(messageClass);
@@ -108,13 +108,13 @@ namespace Tapeti.Connection
///
- public async Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory)
+ public async Task PublishDirect(object message, string queueName, IMessageProperties? properties, bool mandatory)
{
await Publish(message, properties, null, queueName, mandatory);
}
- private async Task Publish(object message, IMessageProperties properties, string exchange, string routingKey, bool mandatory)
+ private async Task Publish(object message, IMessageProperties? properties, string? exchange, string routingKey, bool mandatory)
{
var writableProperties = new MessageProperties(properties);
@@ -151,11 +151,11 @@ namespace Tapeti.Connection
private class PublishContext : IPublishContext
{
- public ITapetiConfig Config { get; set; }
- public string Exchange { get; set; }
- public string RoutingKey { get; set; }
- public object Message { get; set; }
- public IMessageProperties Properties { get; set; }
+ public ITapetiConfig Config { get; init; } = null!;
+ public string? Exchange { get; set; }
+ public string RoutingKey { get; init; } = null!;
+ public object Message { get; init; } = null!;
+ public IMessageProperties? Properties { get; init; }
}
}
}
diff --git a/Tapeti/Connection/TapetiSubscriber.cs b/Tapeti/Connection/TapetiSubscriber.cs
index c8133cc..ad3bab8 100644
--- a/Tapeti/Connection/TapetiSubscriber.cs
+++ b/Tapeti/Connection/TapetiSubscriber.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tapeti.Config;
+using Tapeti.Helpers;
namespace Tapeti.Connection
{
@@ -15,7 +16,7 @@ namespace Tapeti.Connection
private bool consuming;
private readonly List consumerTags = new();
- private CancellationTokenSource initializeCancellationTokenSource;
+ private CancellationTokenSource? initializeCancellationTokenSource;
public TapetiSubscriber(Func clientFactory, ITapetiConfig config)
@@ -71,23 +72,20 @@ namespace Tapeti.Connection
///
public void Reconnect()
{
- CancellationToken cancellationToken;
-
initializeCancellationTokenSource?.Cancel();
initializeCancellationTokenSource = new CancellationTokenSource();
consumerTags.Clear();
- cancellationToken = initializeCancellationTokenSource.Token;
+ var cancellationToken = initializeCancellationTokenSource.Token;
- // ReSharper disable once MethodSupportsCancellation
Task.Run(async () =>
{
await ApplyBindings(cancellationToken);
if (consuming && !cancellationToken.IsCancellationRequested)
await ConsumeQueues(cancellationToken);
- });
+ }, CancellationToken.None);
}
@@ -120,7 +118,7 @@ namespace Tapeti.Connection
}
- private async Task ApplyBindings(CancellationToken cancellationToken)
+ private async ValueTask ApplyBindings(CancellationToken cancellationToken)
{
var routingKeyStrategy = config.DependencyResolver.Resolve();
var exchangeStrategy = config.DependencyResolver.Resolve();
@@ -134,22 +132,33 @@ namespace Tapeti.Connection
else
bindingTarget = new NoVerifyBindingTarget(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken);
- await Task.WhenAll(config.Bindings.Select(binding => binding.Apply(bindingTarget)));
+ foreach (var binding in config.Bindings)
+ await binding.Apply(bindingTarget);
+
await bindingTarget.Apply();
}
private async Task ConsumeQueues(CancellationToken cancellationToken)
{
- var queues = config.Bindings.GroupBy(binding => binding.QueueName);
-
- consumerTags.AddRange((await Task.WhenAll(queues.Select(async group =>
+ var queues = config.Bindings.GroupBy(binding =>
{
- var queueName = group.Key;
- var consumer = new TapetiConsumer(cancellationToken, config, queueName, group);
+ if (string.IsNullOrEmpty(binding.QueueName))
+ throw new InvalidOperationException("QueueName must not be empty");
- return await clientFactory().Consume(cancellationToken, queueName, consumer);
- }))).Where(t => t != null));
+ return binding.QueueName;
+ });
+
+ consumerTags.AddRange(
+ (await Task.WhenAll(queues.Select(async group =>
+ {
+ var queueName = group.Key;
+ var consumer = new TapetiConsumer(cancellationToken, config, queueName, group);
+
+ return await clientFactory().Consume(queueName, consumer, cancellationToken);
+ })))
+ .Where(t => t?.ConsumerTag != null)
+ .Cast());
}
@@ -164,6 +173,7 @@ namespace Tapeti.Connection
{
public string QueueName;
public List MessageClasses;
+ public IRabbitMQArguments? Arguments;
}
private readonly Dictionary> dynamicQueues = new();
@@ -184,38 +194,38 @@ namespace Tapeti.Connection
}
- public abstract Task BindDurable(Type messageClass, string queueName);
- public abstract Task BindDurableDirect(string queueName);
- public abstract Task BindDurableObsolete(string queueName);
+ public abstract ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments);
+ public abstract ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments);
+ public abstract ValueTask BindDurableObsolete(string queueName);
- public async Task BindDynamic(Type messageClass, string queuePrefix = null)
+ public async ValueTask BindDynamic(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{
- var result = await DeclareDynamicQueue(messageClass, queuePrefix);
+ var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
if (!result.IsNewMessageClass)
return result.QueueName;
var routingKey = RoutingKeyStrategy.GetRoutingKey(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;
}
- public async Task BindDynamicDirect(Type messageClass, string queuePrefix = null)
+ public async ValueTask BindDynamicDirect(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{
- var result = await DeclareDynamicQueue(messageClass, queuePrefix);
+ var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
return result.QueueName;
}
- public async Task BindDynamicDirect(string queuePrefix = null)
+ public async ValueTask BindDynamicDirect(string? queuePrefix, IRabbitMQArguments? arguments)
{
// 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.
- return await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix);
+ return await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
}
@@ -225,7 +235,7 @@ namespace Tapeti.Connection
public bool IsNewMessageClass;
}
- private async Task DeclareDynamicQueue(Type messageClass, string queuePrefix)
+ private async Task DeclareDynamicQueue(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{
// Group by prefix
var key = queuePrefix ?? "";
@@ -240,7 +250,7 @@ namespace Tapeti.Connection
foreach (var existingQueueInfo in prefixQueues)
{
// 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
var result = new DeclareDynamicQueueResult
@@ -257,11 +267,12 @@ namespace Tapeti.Connection
}
// Declare a new queue
- var queueName = await ClientFactory().DynamicQueueDeclare(CancellationToken, queuePrefix);
+ var queueName = await ClientFactory().DynamicQueueDeclare(queuePrefix, arguments, CancellationToken);
var queueInfo = new DynamicQueueInfo
{
QueueName = queueName,
- MessageClasses = new List { messageClass }
+ MessageClasses = new List { messageClass },
+ Arguments = arguments
};
prefixQueues.Add(queueInfo);
@@ -277,7 +288,14 @@ namespace Tapeti.Connection
private class DeclareDurableQueuesBindingTarget : CustomBindingTarget
{
- private readonly Dictionary> durableQueues = new();
+ private struct DurableQueueInfo
+ {
+ public List MessageClasses;
+ public IRabbitMQArguments? Arguments;
+ }
+
+
+ private readonly Dictionary durableQueues = new();
private readonly HashSet obsoleteDurableQueues = new();
@@ -286,38 +304,59 @@ namespace Tapeti.Connection
}
- public override Task BindDurable(Type messageClass, string queueName)
+ public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{
// Collect the message classes per queue so we can determine afterwards
// if any of the bindings currently set on the durable queue are no
// 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
+ durableQueues.Add(queueName, new DurableQueueInfo
{
- messageClass
+ MessageClasses = new List
+ {
+ messageClass
+ },
+ Arguments = arguments
});
}
- else if (!messageClasses.Contains(messageClass))
- messageClasses.Add(messageClass);
+ else
+ {
+ if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
+ throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
- return Task.CompletedTask;
- }
+ if (!durableQueueInfo.MessageClasses.Contains(messageClass))
+ durableQueueInfo.MessageClasses.Add(messageClass);
+ }
+
+ return default;
+ }
- public override Task BindDurableDirect(string queueName)
+ public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{
- if (!durableQueues.ContainsKey(queueName))
- durableQueues.Add(queueName, new List());
+ if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
+ {
+ durableQueues.Add(queueName, new DurableQueueInfo
+ {
+ MessageClasses = new List(),
+ Arguments = arguments
+ });
+ }
+ else
+ {
+ if (!durableQueueInfo.Arguments.NullSafeSameValues(arguments))
+ throw new TopologyConfigurationException($"Multiple conflicting QueueArguments attributes specified for queue {queueName}");
+ }
- return Task.CompletedTask;
+ return default;
}
- public override Task BindDurableObsolete(string queueName)
+ public override ValueTask BindDurableObsolete(string queueName)
{
obsoleteDurableQueues.Add(queueName);
- return Task.CompletedTask;
+ return default;
}
@@ -333,7 +372,7 @@ namespace Tapeti.Connection
{
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 routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
@@ -341,7 +380,7 @@ namespace Tapeti.Connection
return new QueueBinding(exchange, routingKey);
});
- await client.DurableQueueDeclare(CancellationToken, queue.Key, bindings);
+ await client.DurableQueueDeclare(queue.Key, bindings, queue.Value.Arguments, CancellationToken);
}));
}
@@ -350,7 +389,7 @@ namespace Tapeti.Connection
{
await Task.WhenAll(obsoleteDurableQueues.Except(durableQueues.Keys).Select(async queue =>
{
- await client.DurableQueueDelete(CancellationToken, queue);
+ await client.DurableQueueDelete(queue, true, CancellationToken);
}));
}
}
@@ -358,7 +397,7 @@ namespace Tapeti.Connection
private class PassiveDurableQueuesBindingTarget : CustomBindingTarget
{
- private readonly List durableQueues = new();
+ private readonly HashSet durableQueues = new();
public PassiveDurableQueuesBindingTarget(Func clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy, CancellationToken cancellationToken) : base(clientFactory, routingKeyStrategy, exchangeStrategy, cancellationToken)
@@ -366,29 +405,28 @@ namespace Tapeti.Connection
}
- public override async Task BindDurable(Type messageClass, string queueName)
+ public override async ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{
- await VerifyDurableQueue(queueName);
+ await VerifyDurableQueue(queueName, arguments);
}
- public override async Task BindDurableDirect(string queueName)
+ public override async ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{
- await VerifyDurableQueue(queueName);
+ await VerifyDurableQueue(queueName, arguments);
}
- public override Task BindDurableObsolete(string queueName)
+ public override ValueTask BindDurableObsolete(string queueName)
{
- return Task.CompletedTask;
+ return default;
}
- private async Task VerifyDurableQueue(string queueName)
+ private async Task VerifyDurableQueue(string queueName, IRabbitMQArguments? arguments)
{
- if (!durableQueues.Contains(queueName))
- {
- await ClientFactory().DurableQueueVerify(CancellationToken, queueName);
- durableQueues.Add(queueName);
- }
+ if (!durableQueues.Add(queueName))
+ return;
+
+ await ClientFactory().DurableQueueVerify(queueName, arguments, CancellationToken);
}
}
@@ -400,19 +438,19 @@ namespace Tapeti.Connection
}
- public override Task BindDurable(Type messageClass, string queueName)
+ public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{
- return Task.CompletedTask;
+ return default;
}
- public override Task BindDurableDirect(string queueName)
+ public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{
- return Task.CompletedTask;
+ return default;
}
- public override Task BindDurableObsolete(string queueName)
+ public override ValueTask BindDurableObsolete(string queueName)
{
- return Task.CompletedTask;
+ return default;
}
}
}
diff --git a/Tapeti/Default/CancellationTokenBinding.cs b/Tapeti/Default/CancellationTokenBinding.cs
index 01c8a72..531ffaf 100644
--- a/Tapeti/Default/CancellationTokenBinding.cs
+++ b/Tapeti/Default/CancellationTokenBinding.cs
@@ -5,7 +5,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Binds a parameter of type CancellationToken to a token which is cancelled when the RabbitMQ connection is closed.
/// Similar to and very much inspired by ASP.NET's RequestAborted CancellationToken.
diff --git a/Tapeti/Default/ConsoleLogger.cs b/Tapeti/Default/ConsoleLogger.cs
index b1a66ca..e195c23 100644
--- a/Tapeti/Default/ConsoleLogger.cs
+++ b/Tapeti/Default/ConsoleLogger.cs
@@ -1,13 +1,12 @@
using System;
-using System.Collections.Generic;
using System.Text;
using Tapeti.Config;
+using Tapeti.Connection;
// ReSharper disable UnusedMember.Global - public API
namespace Tapeti.Default
{
- ///
///
/// Default ILogger implementation for console applications.
///
@@ -81,8 +80,17 @@ namespace Tapeti.Default
}
///
- public void QueueExistsWarning(string queueName, Dictionary arguments)
+ public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? 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(IRabbitMQArguments? arguments)
+ {
+ if (arguments == null || arguments.Count == 0)
+ return "empty";
+
var argumentsText = new StringBuilder();
foreach (var pair in arguments)
{
@@ -91,10 +99,11 @@ namespace Tapeti.Default
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();
}
+
///
public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
{
diff --git a/Tapeti/Default/ControllerBindingContext.cs b/Tapeti/Default/ControllerBindingContext.cs
index 57ee673..8d96345 100644
--- a/Tapeti/Default/ControllerBindingContext.cs
+++ b/Tapeti/Default/ControllerBindingContext.cs
@@ -26,7 +26,7 @@ namespace Tapeti.Default
///
- public Type MessageClass { get; set; }
+ public Type? MessageClass { get; set; }
///
public bool HasMessageClass => MessageClass != null;
@@ -44,10 +44,12 @@ namespace Tapeti.Default
public IBindingResult Result => result;
- public ControllerBindingContext(IEnumerable parameters, ParameterInfo result)
+ public ControllerBindingContext(Type controller, MethodInfo method, IEnumerable parameters, ParameterInfo result)
{
- this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList();
+ Controller = controller;
+ Method = method;
+ this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList();
this.result = new ControllerBindingResult(result);
}
@@ -84,7 +86,13 @@ namespace Tapeti.Default
///
public IEnumerable GetParameterHandlers()
{
- return parameters.Select(p => p.Binding);
+ return parameters.Select(p =>
+ {
+ if (p.Binding == null)
+ throw new TopologyConfigurationException($"No Binding for parameter {p.Info.Name}");
+
+ return p.Binding;
+ });
}
@@ -92,14 +100,13 @@ namespace Tapeti.Default
/// Returns the configured result handler.
///
///
- public ResultHandler GetResultHandler()
+ public ResultHandler? GetResultHandler()
{
return result.Handler;
}
}
- ///
///
/// Default implementation for IBindingParameter
///
@@ -108,7 +115,7 @@ namespace Tapeti.Default
///
/// Provides access to the configured binding.
///
- public ValueFactory Binding { get; set; }
+ public ValueFactory? Binding { get; set; }
///
@@ -139,7 +146,6 @@ namespace Tapeti.Default
}
- ///
///
/// Default implementation for IBindingResult
///
@@ -148,7 +154,7 @@ namespace Tapeti.Default
///
/// Provides access to the configured handler.
///
- public ResultHandler Handler { get; set; }
+ public ResultHandler? Handler { get; set; }
///
diff --git a/Tapeti/Default/ControllerMethodBinding.cs b/Tapeti/Default/ControllerMethodBinding.cs
index 03b44da..10d8ae4 100644
--- a/Tapeti/Default/ControllerMethodBinding.cs
+++ b/Tapeti/Default/ControllerMethodBinding.cs
@@ -4,11 +4,11 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Tapeti.Config;
+using Tapeti.Connection;
using Tapeti.Helpers;
namespace Tapeti.Default
{
- ///
///
/// Binding implementation for controller methods. Do not instantiate this class yourself,
/// instead use the ITapetiConfigBuilder RegisterController / RegisterAllControllers extension
@@ -60,7 +60,7 @@ namespace Tapeti.Default
///
/// The return value handler.
///
- public ResultHandler ResultHandler;
+ public ResultHandler? ResultHandler;
///
@@ -87,10 +87,10 @@ namespace Tapeti.Default
///
- public string QueueName { get; private set; }
+ public string? QueueName { get; private set; }
///
- public QueueType QueueType => bindingInfo.QueueInfo.QueueType;
+ public QueueType? QueueType => bindingInfo.QueueInfo.QueueType;
///
public Type Controller => bindingInfo.ControllerType;
@@ -109,29 +109,29 @@ namespace Tapeti.Default
///
- public async Task Apply(IBindingTarget target)
+ public async ValueTask Apply(IBindingTarget target)
{
if (!bindingInfo.IsObsolete)
{
switch (bindingInfo.BindingTargetMode)
{
case BindingTargetMode.Default:
- if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
- QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
+ if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Dynamic)
+ QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else
{
- await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
+ await target.BindDurable(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name;
}
break;
case BindingTargetMode.Direct:
- if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic)
- QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name);
+ if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Dynamic)
+ QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else
{
- await target.BindDurableDirect(bindingInfo.QueueInfo.Name);
+ await target.BindDurableDirect(bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
QueueName = bindingInfo.QueueInfo.Name;
}
@@ -141,7 +141,7 @@ namespace Tapeti.Default
throw new ArgumentOutOfRangeException(nameof(bindingInfo.BindingTargetMode), bindingInfo.BindingTargetMode, "Invalid BindingTargetMode");
}
}
- else if (bindingInfo.QueueInfo.QueueType == QueueType.Durable)
+ else if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Durable)
{
await target.BindDurableObsolete(bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name;
@@ -157,10 +157,13 @@ namespace Tapeti.Default
///
- public async Task Invoke(IMessageContext context)
+ public async ValueTask Invoke(IMessageContext context)
{
- var controller = dependencyResolver.Resolve(bindingInfo.ControllerType);
- context.Store(new ControllerMessageContextPayload(controller, context.Binding as IControllerMethodBinding));
+ if (context.Binding == null)
+ throw new InvalidOperationException("Invoke should not be called on a context without a binding");
+
+ var controller = Method.IsStatic ? null : dependencyResolver.Resolve(bindingInfo.ControllerType);
+ context.Store(new ControllerMessageContextPayload(controller, (IControllerMethodBinding)context.Binding));
if (!await FilterAllowed(context))
return;
@@ -174,12 +177,12 @@ namespace Tapeti.Default
///
- public async Task Cleanup(IMessageContext context, ConsumeResult consumeResult)
+ public async ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult)
{
await MiddlewareHelper.GoAsync(
bindingInfo.CleanupMiddleware,
async (handler, next) => await handler.Cleanup(context, consumeResult, next),
- () => Task.CompletedTask);
+ () => default);
}
@@ -192,42 +195,43 @@ namespace Tapeti.Default
() =>
{
allowed = true;
- return Task.CompletedTask;
+ return default;
});
return allowed;
}
- private delegate Task MessageHandlerFunc(IMessageContext context);
+ private delegate ValueTask MessageHandlerFunc(IMessageContext context);
- private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable parameterFactories, ResultHandler resultHandler)
+ private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable parameterFactories, ResultHandler? resultHandler)
{
if (resultHandler != null)
- return WrapResultHandlerMethod(method, parameterFactories, resultHandler);
+ return WrapResultHandlerMethod(method.CreateExpressionInvoke(), parameterFactories, resultHandler);
if (method.ReturnType == typeof(void))
- return WrapNullMethod(method, parameterFactories);
+ return WrapNullMethod(method.CreateExpressionInvoke(), parameterFactories);
if (method.ReturnType == typeof(Task))
- return WrapTaskMethod(method, parameterFactories);
+ return WrapTaskMethod(method.CreateExpressionInvoke(), parameterFactories);
- if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
- return WrapGenericTaskMethod(method, parameterFactories);
+ if (method.ReturnType == typeof(ValueTask))
+ return WrapValueTaskMethod(method.CreateExpressionInvoke(), parameterFactories);
- return WrapObjectMethod(method, parameterFactories);
+ // Breaking change in Tapeti 2.9: PublishResultBinding or other middleware should have taken care of the return value. If not, don't silently discard it.
+ throw new ArgumentException($"Method {method.Name} on controller {method.DeclaringType?.FullName} returns type {method.ReturnType.FullName}, which can not be handled by Tapeti or any registered middleware");
}
- private MessageHandlerFunc WrapResultHandlerMethod(MethodBase method, IEnumerable parameterFactories, ResultHandler resultHandler)
+ private MessageHandlerFunc WrapResultHandlerMethod(ExpressionInvoke invoke, IEnumerable parameterFactories, ResultHandler resultHandler)
{
return context =>
{
var controllerPayload = context.Get();
try
{
- var result = method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
+ var result = invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
return resultHandler(context, result);
}
catch (Exception e)
@@ -238,15 +242,15 @@ namespace Tapeti.Default
};
}
- private MessageHandlerFunc WrapNullMethod(MethodBase method, IEnumerable parameterFactories)
+ private MessageHandlerFunc WrapNullMethod(ExpressionInvoke invoke, IEnumerable parameterFactories)
{
return context =>
{
var controllerPayload = context.Get();
try
- {
- method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
- return Task.CompletedTask;
+ {
+ invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
+ return default;
}
catch (Exception e)
{
@@ -257,14 +261,14 @@ namespace Tapeti.Default
}
- private MessageHandlerFunc WrapTaskMethod(MethodBase method, IEnumerable parameterFactories)
+ private MessageHandlerFunc WrapTaskMethod(ExpressionInvoke invoke, IEnumerable parameterFactories)
{
return context =>
{
var controllerPayload = context.Get();
try
{
- return (Task) method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
+ return new ValueTask((Task) invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()));
}
catch (Exception e)
{
@@ -275,32 +279,14 @@ namespace Tapeti.Default
}
- private MessageHandlerFunc WrapGenericTaskMethod(MethodBase method, IEnumerable parameterFactories)
- {
- return context =>
- {
- var controllerPayload = context.Get();
- try
- {
- return (Task)method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
- }
- catch (Exception e)
- {
- AddExceptionData(e);
- throw;
- }
- };
- }
-
-
- private MessageHandlerFunc WrapObjectMethod(MethodBase method, IEnumerable parameterFactories)
+ private MessageHandlerFunc WrapValueTaskMethod(ExpressionInvoke invoke, IEnumerable parameterFactories)
{
return context =>
{
var controllerPayload = context.Get();
try
{
- return Task.FromResult(method.Invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray()));
+ return (ValueTask)invoke(controllerPayload.Controller, parameterFactories.Select(p => p(context)).ToArray());
}
catch (Exception e)
{
@@ -313,8 +299,8 @@ namespace Tapeti.Default
private void AddExceptionData(Exception exception)
{
- exception.Data["Tapeti.Controller.Name"] = bindingInfo.ControllerType?.FullName;
- exception.Data["Tapeti.Controller.Method"] = bindingInfo.Method?.Name;
+ exception.Data["Tapeti.Controller.Name"] = bindingInfo.ControllerType.FullName;
+ exception.Data["Tapeti.Controller.Method"] = bindingInfo.Method.Name;
}
@@ -333,11 +319,22 @@ namespace Tapeti.Default
///
public string Name { get; set; }
-
+ ///
+ /// Optional arguments (x-arguments) passed when declaring the queue.
+ ///
+ public IRabbitMQArguments? QueueArguments { get; set; }
+
///
/// Determines if the QueueInfo properties contain a valid combination.
///
public bool IsValid => QueueType == QueueType.Dynamic || !string.IsNullOrEmpty(Name);
+
+
+ public QueueInfo(QueueType queueType, string name)
+ {
+ QueueType = queueType;
+ Name = name;
+ }
}
}
}
diff --git a/Tapeti/Default/DependencyResolverBinding.cs b/Tapeti/Default/DependencyResolverBinding.cs
index 8eb3b9a..3f2c81e 100644
--- a/Tapeti/Default/DependencyResolverBinding.cs
+++ b/Tapeti/Default/DependencyResolverBinding.cs
@@ -4,7 +4,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Attempts to resolve any unhandled parameters to Controller methods using the IoC container.
/// This middleware is included by default in the standard TapetiConfig.
diff --git a/Tapeti/Default/DevNullLogger.cs b/Tapeti/Default/DevNullLogger.cs
index 9e712d7..002aebe 100644
--- a/Tapeti/Default/DevNullLogger.cs
+++ b/Tapeti/Default/DevNullLogger.cs
@@ -3,7 +3,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Default ILogger implementation which does not log anything.
///
diff --git a/Tapeti/Default/FallbackStringEnumConverter.cs b/Tapeti/Default/FallbackStringEnumConverter.cs
index 8967c0e..12a5ab3 100644
--- a/Tapeti/Default/FallbackStringEnumConverter.cs
+++ b/Tapeti/Default/FallbackStringEnumConverter.cs
@@ -4,7 +4,6 @@ using Newtonsoft.Json;
namespace Tapeti.Default
{
- ///
///
/// Converts an to and from its name string value. If an unknown string value is encountered
/// it will translate to 0xDEADBEEF (-559038737) so it can be gracefully handled.
@@ -25,7 +24,7 @@ namespace Tapeti.Default
///
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
@@ -42,7 +41,7 @@ namespace Tapeti.Default
///
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var isNullable = IsNullableType(objectType);
diff --git a/Tapeti/Default/JsonMessageSerializer.cs b/Tapeti/Default/JsonMessageSerializer.cs
index 4ca8ae4..881260b 100644
--- a/Tapeti/Default/JsonMessageSerializer.cs
+++ b/Tapeti/Default/JsonMessageSerializer.cs
@@ -6,7 +6,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// IMessageSerializer implementation for JSON encoding and decoding using Newtonsoft.Json.
///
@@ -47,9 +46,9 @@ namespace Tapeti.Default
///
- public object Deserialize(byte[] body, IMessageProperties properties)
+ public object? Deserialize(byte[] body, IMessageProperties properties)
{
- if (properties.ContentType == null || !properties.ContentType.Equals(ContentType))
+ if (properties.ContentType is not ContentType)
throw new ArgumentException($"content_type must be {ContentType}");
var typeName = properties.GetHeader(ClassTypeHeader);
diff --git a/Tapeti/Default/MessageBinding.cs b/Tapeti/Default/MessageBinding.cs
index 2265a88..4feddfb 100644
--- a/Tapeti/Default/MessageBinding.cs
+++ b/Tapeti/Default/MessageBinding.cs
@@ -3,7 +3,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Gets the message class from the first parameter of a controller method.
/// This middleware is included by default in the standard TapetiConfig.
diff --git a/Tapeti/Default/MessageContext.cs b/Tapeti/Default/MessageContext.cs
index 3b72e77..697ed30 100644
--- a/Tapeti/Default/MessageContext.cs
+++ b/Tapeti/Default/MessageContext.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Tapeti.Config;
@@ -12,28 +13,28 @@ namespace Tapeti.Default
///
- public ITapetiConfig Config { get; set; }
+ public ITapetiConfig Config { get; set; } = null!;
///
- public string Queue { get; set; }
+ public string Queue { get; set; } = null!;
///
- public string Exchange { get; set; }
+ public string Exchange { get; set; } = null!;
///
- public string RoutingKey { get; set; }
+ public string RoutingKey { get; set; } = null!;
///
- public byte[] RawBody { get; set; }
+ public byte[] RawBody { get; set; } = null!;
///
- public object Message { get; set; }
+ public object? Message { get; set; }
///
- public IMessageProperties Properties { get; set; }
+ public IMessageProperties Properties { get; set; } = null!;
///
- public IBinding Binding { get; set; }
+ public IBinding Binding { get; set; } = null!;
///
public CancellationToken ConnectionClosed { get; set; }
@@ -57,7 +58,7 @@ namespace Tapeti.Default
return (T)payloads[typeof(T)];
}
- public bool TryGet(out T payload) where T : IMessageContextPayload
+ public bool TryGet([NotNullWhen(true)] out T? payload) where T : IMessageContextPayload
{
if (payloads.TryGetValue(typeof(T), out var payloadValue))
{
@@ -100,7 +101,7 @@ namespace Tapeti.Default
///
- public bool Get(string key, out T value) where T : class
+ public bool Get(string key, out T? value) where T : class
{
if (!TryGet(out var payload) ||
!payload.TryGetValue(key, out var objectValue))
@@ -109,7 +110,7 @@ namespace Tapeti.Default
return false;
}
- value = (T)objectValue;
+ value = (T?)objectValue;
return true;
}
@@ -132,7 +133,7 @@ namespace Tapeti.Default
}
- public bool TryGetValue(string key, out object value)
+ public bool TryGetValue(string key, out object? value)
{
return items.TryGetValue(key, out value);
}
diff --git a/Tapeti/Default/MessageProperties.cs b/Tapeti/Default/MessageProperties.cs
index 8227934..6c3daab 100644
--- a/Tapeti/Default/MessageProperties.cs
+++ b/Tapeti/Default/MessageProperties.cs
@@ -4,7 +4,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// IMessagePropertiesReader implementation for providing properties manually
///
@@ -14,13 +13,13 @@ namespace Tapeti.Default
///
- public string ContentType { get; set; }
+ public string? ContentType { get; set; }
///
- public string CorrelationId { get; set; }
+ public string? CorrelationId { get; set; }
///
- public string ReplyTo { get; set; }
+ public string? ReplyTo { get; set; }
///
public bool? Persistent { get; set; }
@@ -38,7 +37,7 @@ namespace Tapeti.Default
///
///
- public MessageProperties(IMessageProperties source)
+ public MessageProperties(IMessageProperties? source)
{
if (source == null)
return;
@@ -65,7 +64,7 @@ namespace Tapeti.Default
}
///
- public string GetHeader(string name)
+ public string? GetHeader(string name)
{
return headers.TryGetValue(name, out var value) ? value : null;
}
diff --git a/Tapeti/Default/NackExceptionStrategy.cs b/Tapeti/Default/NackExceptionStrategy.cs
index e760e20..8cf1de7 100644
--- a/Tapeti/Default/NackExceptionStrategy.cs
+++ b/Tapeti/Default/NackExceptionStrategy.cs
@@ -3,7 +3,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Default implementation of an exception strategy which marks the messages as Error.
///
diff --git a/Tapeti/Default/NamespaceMatchExchangeStrategy.cs b/Tapeti/Default/NamespaceMatchExchangeStrategy.cs
index 54d16d0..123fb44 100644
--- a/Tapeti/Default/NamespaceMatchExchangeStrategy.cs
+++ b/Tapeti/Default/NamespaceMatchExchangeStrategy.cs
@@ -3,7 +3,6 @@ using System.Text.RegularExpressions;
namespace Tapeti.Default
{
- ///
///
/// IExchangeStrategy implementation which uses the first identifier in the namespace in lower case,
/// skipping the first identifier if it is 'Messaging'.
diff --git a/Tapeti/Default/PublishResultBinding.cs b/Tapeti/Default/PublishResultBinding.cs
index dd0bee9..efdc8a0 100644
--- a/Tapeti/Default/PublishResultBinding.cs
+++ b/Tapeti/Default/PublishResultBinding.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
@@ -8,7 +8,6 @@ using Tapeti.Helpers;
namespace Tapeti.Default
{
- ///
///
/// Attempts to publish a return value for Controller methods as a response to the incoming message.
///
@@ -23,7 +22,7 @@ namespace Tapeti.Default
return;
- var hasClassResult = context.Result.Info.ParameterType.IsTypeOrTaskOf(t => t.IsClass, out var isTaskOf, out var actualType);
+ var hasClassResult = context.Result.Info.ParameterType.IsTypeOrTaskOf(t => t.IsClass, out var taskType, out var actualType);
var request = context.MessageClass?.GetCustomAttribute();
var expectedClassResult = request?.Response;
@@ -32,35 +31,63 @@ namespace Tapeti.Default
// Tapeti 1.2: if you just want to publish another message as a result of the incoming message, explicitly call IPublisher.Publish.
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (!hasClassResult && expectedClassResult != null || hasClassResult && expectedClassResult != actualType)
- throw new ArgumentException($"Message handler must return type {expectedClassResult?.FullName ?? "void"} in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}, found: {actualType?.FullName ?? "void"}");
+ throw new ArgumentException($"Message handler for non-request message type {context.MessageClass?.FullName} must return type {expectedClassResult?.FullName ?? "void"} in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}, found: {actualType.FullName ?? "void"}");
if (!hasClassResult)
return;
- if (isTaskOf)
+ switch (taskType)
{
- var handler = GetType().GetMethod("PublishGenericTaskResult", BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType);
- Debug.Assert(handler != null, nameof(handler) + " != null");
+ case TaskType.None:
+ context.Result.SetHandler((messageContext, value) => Reply(value, messageContext));
+ break;
- context.Result.SetHandler(async (messageContext, value) => { await (Task) handler.Invoke(null, new[] {messageContext, value }); });
+ case TaskType.Task:
+ var handler = GetType().GetMethod(nameof(PublishGenericTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType);
+ Debug.Assert(handler != null, nameof(handler) + " != null");
+
+ context.Result.SetHandler((messageContext, value) =>
+ {
+ var result = handler.Invoke(null, new[] { messageContext, value });
+ return result != null ? (ValueTask)result : ValueTask.CompletedTask;
+ });
+ break;
+
+ case TaskType.ValueTask:
+ var valueTaskHandler = GetType().GetMethod(nameof(PublishGenericValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType);
+ Debug.Assert(valueTaskHandler != null, nameof(handler) + " != null");
+
+ context.Result.SetHandler((messageContext, value) =>
+ {
+ var result = valueTaskHandler.Invoke(null, new[] { messageContext, value });
+ return result != null ? (ValueTask)result : ValueTask.CompletedTask;
+ });
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- else
- context.Result.SetHandler((messageContext, value) => Reply(value, messageContext));
}
- // ReSharper disable once UnusedMember.Local - used implicitly above
- private static async Task PublishGenericTaskResult(IMessageContext messageContext, object value) where T : class
+ private static async ValueTask PublishGenericTaskResult(IMessageContext messageContext, object value) where T : class
{
var message = await (Task)value;
await Reply(message, messageContext);
}
- private static Task Reply(object message, IMessageContext messageContext)
+ private static async ValueTask PublishGenericValueTaskResult(IMessageContext messageContext, object value) where T : class
+ {
+ var message = await (ValueTask)value;
+ await Reply(message, messageContext);
+ }
+
+
+ private static async ValueTask Reply(object? message, IMessageContext messageContext)
{
if (message == null)
throw new ArgumentException("Return value of a request message handler must not be null");
@@ -71,9 +98,10 @@ namespace Tapeti.Default
CorrelationId = messageContext.Properties.CorrelationId
};
- return !string.IsNullOrEmpty(messageContext.Properties.ReplyTo)
- ? publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties, messageContext.Properties.Persistent.GetValueOrDefault(true))
- : publisher.Publish(message, properties, false);
+ if (!string.IsNullOrEmpty(messageContext.Properties.ReplyTo))
+ await publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties, messageContext.Properties.Persistent.GetValueOrDefault(true));
+ else
+ await publisher.Publish(message, properties, false);
}
}
}
diff --git a/Tapeti/Default/RabbitMQMessageProperties.cs b/Tapeti/Default/RabbitMQMessageProperties.cs
index cdce19b..d4d90e2 100644
--- a/Tapeti/Default/RabbitMQMessageProperties.cs
+++ b/Tapeti/Default/RabbitMQMessageProperties.cs
@@ -15,21 +15,21 @@ namespace Tapeti.Default
///
- public string ContentType
+ public string? ContentType
{
get => BasicProperties.IsContentTypePresent() ? BasicProperties.ContentType : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ContentType = value; else BasicProperties.ClearContentType(); }
}
///
- public string CorrelationId
+ public string? CorrelationId
{
get => BasicProperties.IsCorrelationIdPresent() ? BasicProperties.CorrelationId : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.CorrelationId = value; else BasicProperties.ClearCorrelationId(); }
}
///
- public string ReplyTo
+ public string? ReplyTo
{
get => BasicProperties.IsReplyToPresent() ? BasicProperties.ReplyTo : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ReplyTo = value; else BasicProperties.ClearReplyTo(); }
@@ -66,7 +66,7 @@ namespace Tapeti.Default
///
///
- public RabbitMQMessageProperties(IBasicProperties basicProperties, IMessageProperties source)
+ public RabbitMQMessageProperties(IBasicProperties basicProperties, IMessageProperties? source)
{
BasicProperties = basicProperties;
if (source == null)
@@ -97,7 +97,7 @@ namespace Tapeti.Default
///
- public string GetHeader(string name)
+ public string? GetHeader(string name)
{
if (BasicProperties.Headers == null)
return null;
diff --git a/Tapeti/Default/RequeueExceptionStrategy.cs b/Tapeti/Default/RequeueExceptionStrategy.cs
index f91922b..987e954 100644
--- a/Tapeti/Default/RequeueExceptionStrategy.cs
+++ b/Tapeti/Default/RequeueExceptionStrategy.cs
@@ -5,7 +5,6 @@ using Tapeti.Config;
namespace Tapeti.Default
{
- ///
///
/// Example exception strategy which requeues all messages that result in an error.
///
diff --git a/Tapeti/Default/TypeNameRoutingKeyStrategy.cs b/Tapeti/Default/TypeNameRoutingKeyStrategy.cs
index 35ee8d8..5a6b2b2 100644
--- a/Tapeti/Default/TypeNameRoutingKeyStrategy.cs
+++ b/Tapeti/Default/TypeNameRoutingKeyStrategy.cs
@@ -7,7 +7,6 @@ using Tapeti.Helpers;
namespace Tapeti.Default
{
- ///
///
/// IRoutingKeyStrategy implementation which transforms the class name into a dot-separated routing key based
/// on the casing. Accounts for acronyms. If the class name ends with 'Message' it is not included in the routing key.
@@ -61,7 +60,7 @@ namespace Tapeti.Default
}
- private static List SplitPascalCase(string value)
+ private static List? SplitPascalCase(string value)
{
var split = SeparatorRegex.Split(value);
if (split.Length == 0)
diff --git a/Tapeti/Exceptions/NackException.cs b/Tapeti/Exceptions/NackException.cs
index a2fb7fa..6d2e5e9 100644
--- a/Tapeti/Exceptions/NackException.cs
+++ b/Tapeti/Exceptions/NackException.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Exceptions
{
- ///
///
/// Raised when a message is nacked by the message bus.
///
diff --git a/Tapeti/Exceptions/NoRouteException.cs b/Tapeti/Exceptions/NoRouteException.cs
index 3f1ac64..96d865e 100644
--- a/Tapeti/Exceptions/NoRouteException.cs
+++ b/Tapeti/Exceptions/NoRouteException.cs
@@ -2,7 +2,6 @@
namespace Tapeti.Exceptions
{
- ///
///
/// Raised when a mandatory message has no route.
///
diff --git a/Tapeti/Helpers/ConnectionstringParser.cs b/Tapeti/Helpers/ConnectionstringParser.cs
index f49f35a..c872a79 100644
--- a/Tapeti/Helpers/ConnectionstringParser.cs
+++ b/Tapeti/Helpers/ConnectionstringParser.cs
@@ -1,5 +1,7 @@
using System.Text;
+// ReSharper disable UnusedMember.Global - public API
+
namespace Tapeti.Helpers
{
///
diff --git a/Tapeti/Helpers/DictionaryHelper.cs b/Tapeti/Helpers/DictionaryHelper.cs
new file mode 100644
index 0000000..93af905
--- /dev/null
+++ b/Tapeti/Helpers/DictionaryHelper.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace Tapeti.Helpers
+{
+ ///
+ /// Provides extension methods for dictionaries.
+ ///
+ public static class DictionaryHelper
+ {
+ ///
+ /// Checks if two dictionaries are considered compatible. If either is null they are considered empty.
+ ///
+ public static bool NullSafeSameValues(this IReadOnlyDictionary? arguments1, IReadOnlyDictionary? 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;
+ }
+ }
+}
diff --git a/Tapeti/Helpers/ExpressionInvoker.cs b/Tapeti/Helpers/ExpressionInvoker.cs
new file mode 100644
index 0000000..ea573eb
--- /dev/null
+++ b/Tapeti/Helpers/ExpressionInvoker.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+
+// Note: I also tried a version which accepts a ValueFactory[] to reduce the object array allocations,
+// but the performance benefits were negligable and it still allocated more memory than expected.
+//
+// Reflection.Emit is another option which I've dabbled with, but that's too much of a risk for me to
+// attempt at the moment and there's probably other code which could benefit from optimization more.
+
+namespace Tapeti.Helpers
+{
+ ///
+ /// The precompiled version of MethodInfo.Invoke.
+ ///
+ /// The instance on which the method should be called.
+ /// The arguments passed to the method.
+ public delegate object ExpressionInvoke(object? target, params object?[] args);
+
+
+ ///
+ /// Provides a way to create a precompiled version of MethodInfo.Invoke with decreased overhead.
+ ///
+ public static class ExpressionInvokeExtensions
+ {
+ ///
+ /// Creates a precompiled version of MethodInfo.Invoke with decreased overhead.
+ ///
+ public static ExpressionInvoke CreateExpressionInvoke(this MethodInfo method)
+ {
+ if (method.DeclaringType == null)
+ throw new ArgumentException("Method must have a declaring type");
+
+ var argsParameter = Expression.Parameter(typeof(object[]), "args");
+ var parameters = method.GetParameters().Select(
+ (p, i) =>
+ {
+ var argsIndexExpression = Expression.Constant(i, typeof(int));
+ var argExpression = Expression.ArrayIndex(argsParameter, argsIndexExpression);
+
+ return Expression.Convert(argExpression, p.ParameterType) as Expression;
+ })
+ .ToArray();
+
+
+ var instanceParameter = Expression.Parameter(typeof(object), "target");
+ Expression? instance = method.IsStatic
+ ? null
+ : Expression.Convert(instanceParameter, method.DeclaringType);
+
+ var invoke = Expression.Call(instance, method, parameters);
+
+ Expression lambda;
+
+ if (method.ReturnType != typeof(void))
+ {
+ var result = Expression.Convert(invoke, typeof(object));
+ lambda = Expression.Lambda(result, instanceParameter, argsParameter);
+ }
+ else
+ {
+ var nullResult = Expression.Constant(null, typeof(object));
+ var body = Expression.Block(invoke, nullResult);
+ lambda = Expression.Lambda(body, instanceParameter, argsParameter);
+ }
+
+ return lambda.Compile();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tapeti/Helpers/MiddlewareHelper.cs b/Tapeti/Helpers/MiddlewareHelper.cs
index ba1158e..cdd3446 100644
--- a/Tapeti/Helpers/MiddlewareHelper.cs
+++ b/Tapeti/Helpers/MiddlewareHelper.cs
@@ -16,7 +16,7 @@ namespace Tapeti.Helpers
/// Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.
/// The action to execute when the innermost middleware calls next.
///
- public static void Go(IReadOnlyList middleware, Action handle, Action lastHandler)
+ public static void Go(IReadOnlyList? middleware, Action handle, Action lastHandler)
{
var handlerIndex = middleware?.Count - 1 ?? -1;
if (middleware == null || handlerIndex == -1)
@@ -45,7 +45,7 @@ namespace Tapeti.Helpers
/// Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.
/// The action to execute when the innermost middleware calls next.
///
- public static async Task GoAsync(IReadOnlyList middleware, Func, Task> handle, Func lastHandler)
+ public static async ValueTask GoAsync(IReadOnlyList? middleware, Func, ValueTask> handle, Func lastHandler)
{
var handlerIndex = middleware?.Count - 1 ?? -1;
if (middleware == null || handlerIndex == -1)
@@ -54,7 +54,7 @@ namespace Tapeti.Helpers
return;
}
- async Task HandleNext()
+ async ValueTask HandleNext()
{
handlerIndex--;
if (handlerIndex >= 0)
diff --git a/Tapeti/Helpers/TaskTypeHelper.cs b/Tapeti/Helpers/TaskTypeHelper.cs
index 44e0c99..49446b9 100644
--- a/Tapeti/Helpers/TaskTypeHelper.cs
+++ b/Tapeti/Helpers/TaskTypeHelper.cs
@@ -3,30 +3,59 @@ using System.Threading.Tasks;
namespace Tapeti.Helpers
{
+ ///
+ /// Determines if a type is a Task, ValueTask or other type.
+ ///
+ public enum TaskType
+ {
+ ///
+ /// Type is not a Task or ValueTask.
+ ///
+ None,
+
+ ///
+ /// Type is a Task or Task<T>
+ ///
+ Task,
+
+ ///
+ /// Type is a ValueTask or ValueTask<T>
+ ///
+ ValueTask
+ }
+
+
///
/// Helper methods for working with synchronous and asynchronous versions of methods.
///
public static class TaskTypeHelper
{
///
- /// Determines if the given type matches the predicate, taking Task types into account.
+ /// Determines if the given type matches the predicate, taking Task and ValueTask types into account.
///
///
///
- ///
+ ///
///
- public static bool IsTypeOrTaskOf(this Type type, Func predicate, out bool isTaskOf, out Type actualType)
+ public static bool IsTypeOrTaskOf(this Type type, Func predicate, out TaskType taskType, out Type actualType)
{
if (type == typeof(Task))
{
- isTaskOf = false;
+ taskType = TaskType.Task;
+ actualType = type;
+ return false;
+ }
+
+ if (type == typeof(ValueTask))
+ {
+ taskType = TaskType.ValueTask;
actualType = type;
return false;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
{
- isTaskOf = true;
+ taskType = TaskType.Task;
var genericArguments = type.GetGenericArguments();
if (genericArguments.Length == 1 && predicate(genericArguments[0]))
@@ -36,7 +65,19 @@ namespace Tapeti.Helpers
}
}
- isTaskOf = false;
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>))
+ {
+ taskType = TaskType.ValueTask;
+
+ var genericArguments = type.GetGenericArguments();
+ if (genericArguments.Length == 1 && predicate(genericArguments[0]))
+ {
+ actualType = genericArguments[0];
+ return true;
+ }
+ }
+
+ taskType = TaskType.None;
actualType = type;
return predicate(type);
}
@@ -47,10 +88,10 @@ namespace Tapeti.Helpers
///
///
///
- ///
- public static bool IsTypeOrTaskOf(this Type type, Func predicate, out bool isTaskOf)
+ ///
+ public static bool IsTypeOrTaskOf(this Type type, Func predicate, out TaskType taskType)
{
- return IsTypeOrTaskOf(type, predicate, out isTaskOf, out _);
+ return IsTypeOrTaskOf(type, predicate, out taskType, out _);
}
@@ -59,10 +100,10 @@ namespace Tapeti.Helpers
///
///
///
- ///
- public static bool IsTypeOrTaskOf(this Type type, Type compareTo, out bool isTaskOf)
+ ///
+ public static bool IsTypeOrTaskOf(this Type type, Type compareTo, out TaskType taskType)
{
- return IsTypeOrTaskOf(type, t => t == compareTo, out isTaskOf);
+ return IsTypeOrTaskOf(type, t => t == compareTo, out taskType);
}
}
}
diff --git a/Tapeti/IConnection.cs b/Tapeti/IConnection.cs
index 8aab48a..da478e0 100644
--- a/Tapeti/IConnection.cs
+++ b/Tapeti/IConnection.cs
@@ -7,19 +7,27 @@ using System.Threading.Tasks;
namespace Tapeti
{
///
- ///
+ /// Contains information about the established connection.
///
public class ConnectedEventArgs
{
///
/// The connection parameters used to establish the connection.
///
- public TapetiConnectionParams ConnectionParams;
+ public TapetiConnectionParams ConnectionParams { get; }
///
/// The local port for the connection. Useful for identifying the connection in the management interface.
///
- public int LocalPort;
+ public int LocalPort { get; }
+
+
+ ///
+ public ConnectedEventArgs(TapetiConnectionParams connectionParams, int localPort)
+ {
+ ConnectionParams = connectionParams;
+ LocalPort = localPort;
+ }
}
@@ -31,12 +39,20 @@ namespace Tapeti
///
/// The ReplyCode as indicated by the client library
///
- public ushort ReplyCode;
+ public ushort ReplyCode { get; }
///
/// The ReplyText as indicated by the client library
///
- public string ReplyText;
+ public string ReplyText { get; }
+
+
+ ///
+ public DisconnectedEventArgs(ushort replyCode, string replyText)
+ {
+ ReplyCode = replyCode;
+ ReplyText = replyText;
+ }
}
diff --git a/Tapeti/IDependencyResolver.cs b/Tapeti/IDependencyResolver.cs
index 12870bf..002bec1 100644
--- a/Tapeti/IDependencyResolver.cs
+++ b/Tapeti/IDependencyResolver.cs
@@ -23,7 +23,6 @@ namespace Tapeti
}
- ///
///
/// Allows registering controller classes into the IoC container. Also registers default implementations,
/// so that the calling application may override these.
diff --git a/Tapeti/ILogger.cs b/Tapeti/ILogger.cs
index b9bd22f..e57a7ec 100644
--- a/Tapeti/ILogger.cs
+++ b/Tapeti/ILogger.cs
@@ -1,6 +1,6 @@
using System;
-using System.Collections.Generic;
using Tapeti.Config;
+using Tapeti.Connection;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedMemberInSuper.Global
@@ -24,7 +24,6 @@ namespace Tapeti
}
- ///
///
/// Contains information about the failed connection.
///
@@ -33,11 +32,10 @@ namespace Tapeti
///
/// The exception that caused the connection to fail.
///
- Exception Exception { get; }
+ Exception? Exception { get; }
}
- ///
///
/// Contains information about the established connection.
///
@@ -138,8 +136,9 @@ namespace Tapeti
/// If the queue already exists but should be compatible QueueDeclare will be called instead.
///
/// The name of the queue that is declared
- /// The x-arguments of the existing queue
- void QueueExistsWarning(string queueName, Dictionary arguments);
+ /// The x-arguments of the existing queue
+ /// The x-arguments of the queue that would be declared
+ void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments);
///
/// Called before a binding is added to a queue.
diff --git a/Tapeti/IMessageSerializer.cs b/Tapeti/IMessageSerializer.cs
index b2bbfc4..679e7b2 100644
--- a/Tapeti/IMessageSerializer.cs
+++ b/Tapeti/IMessageSerializer.cs
@@ -21,6 +21,6 @@ namespace Tapeti
/// The encoded message
/// The properties as sent along with the message
/// A decoded instance of the message
- object Deserialize(byte[] body, IMessageProperties properties);
+ object? Deserialize(byte[] body, IMessageProperties properties);
}
}
diff --git a/Tapeti/IPublisher.cs b/Tapeti/IPublisher.cs
index 70cb754..3e0ac58 100644
--- a/Tapeti/IPublisher.cs
+++ b/Tapeti/IPublisher.cs
@@ -29,7 +29,7 @@ namespace Tapeti
///
/// An expression defining the method which handles the response. Example: c => c.HandleResponse
/// The message to send
- Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class;
+ Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class;
///
@@ -42,7 +42,7 @@ namespace Tapeti
///
/// An expression defining the method which handles the response. Example: c => c.HandleResponse
/// The message to send
- Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class;
+ Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class;
///
@@ -54,7 +54,6 @@ namespace Tapeti
}
- ///
///
/// Low-level publisher for Tapeti internal use.
///
@@ -70,7 +69,7 @@ namespace Tapeti
/// An instance of a message class
/// Metadata to include in the message
/// If true, an exception will be raised if the message can not be delivered to at least one queue
- Task Publish(object message, IMessageProperties properties, bool mandatory);
+ Task Publish(object message, IMessageProperties? properties, bool mandatory);
///
@@ -81,6 +80,6 @@ namespace Tapeti
/// Metadata to include in the message
/// If true, an exception will be raised if the message can not be delivered to the queue
///
- Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory);
+ Task PublishDirect(object message, string queueName, IMessageProperties? properties, bool mandatory);
}
}
diff --git a/Tapeti/Properties/AssemblyInfo.cs b/Tapeti/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..eaca9e3
--- /dev/null
+++ b/Tapeti/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Tapeti.Tests")]
\ No newline at end of file
diff --git a/Tapeti/Tapeti.csproj b/Tapeti/Tapeti.csproj
index dcef360..2d1bd84 100644
--- a/Tapeti/Tapeti.csproj
+++ b/Tapeti/Tapeti.csproj
@@ -1,7 +1,7 @@
-
+
- netstandard2.0
+ net6.0;net7.0
true
2.0.0
Mark van Renswoude
@@ -11,18 +11,27 @@
Unlicense
https://github.com/MvRens/Tapeti
Tapeti.png
- latest
+ 9
+ enable
1701;1702
+
+
+
+
+
+
+
+
+
+
-
-
-
+
@@ -33,7 +42,7 @@
-
-
+
+
diff --git a/Tapeti/TapetiAppSettingsConnectionParams.cs b/Tapeti/TapetiAppSettingsConnectionParams.cs
deleted file mode 100644
index 87138b7..0000000
--- a/Tapeti/TapetiAppSettingsConnectionParams.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System.Configuration;
-using System.Linq;
-
-// ReSharper disable UnusedMember.Global
-
-namespace Tapeti
-{
- ///
- ///
- /// Implementation of TapetiConnectionParams which reads the values from the AppSettings.
- ///
- ///
- ///
- /// AppSettings keys
- ///
- /// - rabbitmq:hostname
- /// - rabbitmq:port
- /// - rabbitmq:virtualhost
- /// - rabbitmq:username
- /// - rabbitmq:password
- /// - rabbitmq:prefetchcount
- /// - rabbitmq:managementport
- /// - rabbitmq:clientproperty:*
- ///
- public class TapetiAppSettingsConnectionParams : TapetiConnectionParams
- {
- private const string DefaultPrefix = "rabbitmq:";
- // ReSharper disable InconsistentNaming
- private const string KeyHostname = "hostname";
- private const string KeyPort = "port";
- private const string KeyVirtualHost = "virtualhost";
- private const string KeyUsername = "username";
- private const string KeyPassword = "password";
- private const string KeyPrefetchCount = "prefetchcount";
- private const string KeyManagementPort = "managementport";
- private const string KeyClientProperty = "clientproperty:";
- // ReSharper restore InconsistentNaming
-
-
- private readonly struct AppSettingsKey
- {
- public readonly string Entry;
- public readonly string Parameter;
-
- public AppSettingsKey(string entry, string parameter)
- {
- Entry = entry;
- Parameter = parameter;
- }
- }
-
-
- ///
- ///
- /// The prefix to apply to the keys. Defaults to "rabbitmq:"
- public TapetiAppSettingsConnectionParams(string prefix = DefaultPrefix)
- {
- var keys = !string.IsNullOrEmpty(prefix)
- ? ConfigurationManager.AppSettings.AllKeys.Where(k => k.StartsWith(prefix)).Select(k => new AppSettingsKey(k, k.Substring(prefix.Length)))
- : ConfigurationManager.AppSettings.AllKeys.Select(k => new AppSettingsKey(k, k));
-
-
-
- foreach (var key in keys)
- {
- var value = ConfigurationManager.AppSettings[key.Entry];
-
- if (key.Parameter.StartsWith(KeyClientProperty))
- {
- ClientProperties.Add(key.Parameter.Substring(KeyClientProperty.Length), value);
- }
- else
- {
- // ReSharper disable once SwitchStatementMissingSomeCases - don't fail if we encounter an unknown value
- switch (key.Parameter)
- {
- case KeyHostname: HostName = value; break;
- case KeyPort: Port = int.Parse(value); break;
- case KeyVirtualHost: VirtualHost = value; break;
- case KeyUsername: Username = value; break;
- case KeyPassword: Password = value; break;
- case KeyPrefetchCount: PrefetchCount = ushort.Parse(value); break;
- case KeyManagementPort: ManagementPort = int.Parse(value); break;
- }
- }
- }
- }
- }
-}
diff --git a/Tapeti/TapetiConfig.cs b/Tapeti/TapetiConfig.cs
index ec2234c..ee48d3a 100644
--- a/Tapeti/TapetiConfig.cs
+++ b/Tapeti/TapetiConfig.cs
@@ -15,9 +15,9 @@ namespace Tapeti
/// Default implementation of the Tapeti config builder.
/// Automatically registers the default middleware for injecting the message parameter and handling the return value.
///
- public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess
+ public class TapetiConfig : ITapetiConfigBuilderAccess
{
- private Config config;
+ private Config? config;
private readonly List bindingMiddleware = new();
@@ -92,29 +92,25 @@ namespace Tapeti
var configInstance = GetConfig();
- var middlewareBundle = extension.GetMiddleware(DependencyResolver);
- if (middlewareBundle != null)
+ foreach (var middleware in extension.GetMiddleware(DependencyResolver))
{
- foreach (var middleware in middlewareBundle)
+ switch (middleware)
{
- switch (middleware)
- {
- case IControllerBindingMiddleware bindingExtension:
- Use(bindingExtension);
- break;
+ case IControllerBindingMiddleware bindingExtension:
+ Use(bindingExtension);
+ break;
- case IMessageMiddleware messageExtension:
- configInstance.Use(messageExtension);
- break;
+ case IMessageMiddleware messageExtension:
+ configInstance.Use(messageExtension);
+ break;
- case IPublishMiddleware publishExtension:
- configInstance.Use(publishExtension);
- break;
+ case IPublishMiddleware publishExtension:
+ configInstance.Use(publishExtension);
+ break;
- default:
- throw new ArgumentException(
- $"Unsupported middleware implementation: {middleware?.GetType().Name ?? "null"}");
- }
+ default:
+ throw new ArgumentException(
+ $"Unsupported middleware implementation: {middleware.GetType().Name}");
}
}
@@ -123,7 +119,7 @@ namespace Tapeti
return this;
foreach (var binding in bindingBundle)
- config.RegisterBinding(binding);
+ GetConfig().RegisterBinding(binding);
return this;
}
@@ -189,7 +185,7 @@ namespace Tapeti
///
protected void RegisterDefaults()
{
- if (!(DependencyResolver is IDependencyContainer container))
+ if (DependencyResolver is not IDependencyContainer container)
return;
if (ConsoleHelper.IsAvailable())
@@ -313,17 +309,23 @@ namespace Tapeti
internal class ConfigBindings : List, ITapetiConfigBindings
{
- private Dictionary methodLookup;
+ private Dictionary? methodLookup;
- public IControllerMethodBinding ForMethod(Delegate method)
+ public IControllerMethodBinding? ForMethod(Delegate method)
{
+ if (methodLookup == null)
+ throw new InvalidOperationException("Lock must be called first");
+
return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null;
}
- public IControllerMethodBinding ForMethod(MethodInfo method)
+ public IControllerMethodBinding? ForMethod(MethodInfo method)
{
+ if (methodLookup == null)
+ throw new InvalidOperationException("Lock must be called first");
+
return methodLookup.TryGetValue(method, out var binding) ? binding : null;
}
diff --git a/Tapeti/TapetiConfigControllers.cs b/Tapeti/TapetiConfigControllers.cs
index 9573512..8459445 100644
--- a/Tapeti/TapetiConfigControllers.cs
+++ b/Tapeti/TapetiConfigControllers.cs
@@ -1,15 +1,16 @@
-using System;
+using System;
using System.Linq;
using System.Reflection;
+using System.Text;
using Tapeti.Annotations;
using Tapeti.Config;
+using Tapeti.Connection;
using Tapeti.Default;
// ReSharper disable UnusedMember.Global
namespace Tapeti
{
- ///
///
/// Thrown when an issue is detected in a controller configuration.
///
@@ -37,24 +38,19 @@ namespace Tapeti
if (!controller.IsClass)
throw new ArgumentException($"Controller {controller.Name} must be a class");
- var controllerQueueInfo = GetQueueInfo(controller);
+ var controllerQueueInfo = GetQueueInfo(controller, null);
(builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller);
var controllerIsObsolete = controller.GetCustomAttribute() != null;
- foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance)
+ foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false)
.Select(m => (MethodInfo)m))
{
var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute() != null;
- var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter)
- {
- Controller = controller,
- Method = method
- };
-
+ var context = new ControllerBindingContext(controller, method, method.GetParameters(), method.ReturnParameter);
if (method.GetCustomAttribute() != null)
context.SetBindingTargetMode(BindingTargetMode.Direct);
@@ -78,8 +74,8 @@ namespace Tapeti
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
}
- var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
- if (methodQueueInfo == null || !methodQueueInfo.IsValid)
+ var methodQueueInfo = GetQueueInfo(method, controllerQueueInfo);
+ if (methodQueueInfo is not { IsValid: true })
throw new TopologyConfigurationException(
$"Method {method.Name} or controller {controller.Name} requires a queue attribute");
@@ -124,24 +120,98 @@ namespace Tapeti
///
public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder)
{
- return RegisterAllControllers(builder, Assembly.GetEntryAssembly());
+ var assembly = Assembly.GetEntryAssembly();
+ if (assembly == null)
+ throw new InvalidOperationException("No EntryAssembly");
+
+ return RegisterAllControllers(builder, assembly);
}
- private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member)
+ private static ControllerMethodBinding.QueueInfo? GetQueueInfo(MemberInfo member, ControllerMethodBinding.QueueInfo? fallbackQueueInfo)
{
var dynamicQueueAttribute = member.GetCustomAttribute();
var durableQueueAttribute = member.GetCustomAttribute();
+ var queueArgumentsAttribute = member.GetCustomAttribute();
if (dynamicQueueAttribute != null && durableQueueAttribute != null)
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
- if (dynamicQueueAttribute != null)
- return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix };
+ if (dynamicQueueAttribute == null && durableQueueAttribute == null && (queueArgumentsAttribute == null || fallbackQueueInfo == null))
+ return fallbackQueueInfo;
- return durableQueueAttribute != null
- ? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name }
- : null;
+
+ QueueType queueType;
+ string name;
+
+
+ if (dynamicQueueAttribute != null)
+ {
+ queueType = QueueType.Dynamic;
+ name = dynamicQueueAttribute.Prefix;
+ }
+ else if (durableQueueAttribute != null)
+ {
+ queueType = QueueType.Durable;
+ name = durableQueueAttribute.Name;
+ }
+ else
+ {
+ queueType = fallbackQueueInfo!.QueueType;
+ name = fallbackQueueInfo.Name;
+ }
+
+ return new ControllerMethodBinding.QueueInfo(queueType, name)
+ {
+ QueueArguments = GetQueueArguments(queueArgumentsAttribute) ?? fallbackQueueInfo?.QueueArguments
+ };
+ }
+
+
+ private static IRabbitMQArguments? GetQueueArguments(QueueArgumentsAttribute? queueArgumentsAttribute)
+ {
+ if (queueArgumentsAttribute == null)
+ return null;
+
+ var arguments = new RabbitMQArguments(queueArgumentsAttribute.CustomArguments.ToDictionary(
+ p => p.Key,
+ p => p.Value switch
+ {
+ string stringValue => Encoding.UTF8.GetBytes(stringValue),
+ _ => p.Value
+ }
+ ))
+ {
+
+ };
+ if (queueArgumentsAttribute.MaxLength > 0)
+ arguments.Add(@"x-max-length", queueArgumentsAttribute.MaxLength);
+
+ if (queueArgumentsAttribute.MaxLengthBytes > 0)
+ arguments.Add(@"x-max-length-bytes", queueArgumentsAttribute.MaxLengthBytes);
+
+ if (queueArgumentsAttribute.MessageTTL > 0)
+ arguments.Add(@"x-message-ttl", queueArgumentsAttribute.MessageTTL);
+
+ switch (queueArgumentsAttribute.Overflow)
+ {
+ case RabbitMQOverflow.NotSpecified:
+ break;
+ case RabbitMQOverflow.DropHead:
+ arguments.AddUTF8(@"x-overflow", @"drop-head");
+ break;
+ case RabbitMQOverflow.RejectPublish:
+ arguments.AddUTF8(@"x-overflow", @"reject-publish");
+ break;
+ case RabbitMQOverflow.RejectPublishDeadletter:
+ arguments.AddUTF8(@"x-overflow", @"reject-publish-dlx");
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(queueArgumentsAttribute.Overflow), queueArgumentsAttribute.Overflow, "Unsupported Overflow value");
+ }
+
+
+ return arguments.Count > 0 ? arguments : null;
}
}
}
diff --git a/Tapeti/TapetiConnection.cs b/Tapeti/TapetiConnection.cs
index f26e3ca..8ab7802 100644
--- a/Tapeti/TapetiConnection.cs
+++ b/Tapeti/TapetiConnection.cs
@@ -9,7 +9,6 @@ using Tapeti.Connection;
namespace Tapeti
{
- ///
///
/// Creates a connection to RabbitMQ based on the provided Tapeti config.
///
@@ -25,10 +24,10 @@ namespace Tapeti
/// This property must be set before first subscribing or publishing, otherwise it
/// will use the default connection parameters.
///
- public TapetiConnectionParams Params { get; set; }
+ public TapetiConnectionParams? Params { get; set; }
private readonly Lazy client;
- private TapetiSubscriber subscriber;
+ private TapetiSubscriber? subscriber;
private bool disposed;
@@ -49,13 +48,13 @@ namespace Tapeti
}
///
- public event ConnectedEventHandler Connected;
+ public event ConnectedEventHandler? Connected;
///
- public event DisconnectedEventHandler Disconnected;
+ public event DisconnectedEventHandler? Disconnected;
///
- public event ConnectedEventHandler Reconnected;
+ public event ConnectedEventHandler? Reconnected;
///
@@ -164,7 +163,7 @@ namespace Tapeti
var reconnectedEvent = Reconnected;
if (reconnectedEvent != null)
- Task.Run(() => reconnectedEvent?.Invoke(this, e));
+ Task.Run(() => reconnectedEvent.Invoke(this, e));
}
///
diff --git a/Tapeti/TapetiConnectionParams.cs b/Tapeti/TapetiConnectionParams.cs
index 5e2258e..43f923f 100644
--- a/Tapeti/TapetiConnectionParams.cs
+++ b/Tapeti/TapetiConnectionParams.cs
@@ -10,7 +10,7 @@ namespace Tapeti
///
public class TapetiConnectionParams
{
- private IDictionary clientProperties;
+ private IDictionary? clientProperties;
///
@@ -59,7 +59,7 @@ namespace Tapeti
/// If any of the default keys used by the RabbitMQ Client library (product, version) are specified their value
/// will be overwritten. See DefaultClientProperties in Connection.cs in the RabbitMQ .NET client source for the default values.
///
- public IDictionary ClientProperties {
+ public IDictionary? ClientProperties {
get => clientProperties ??= new Dictionary();
set => clientProperties = value;
}
diff --git a/Tapeti/Tasks/SingleThreadTaskQueue.cs b/Tapeti/Tasks/SingleThreadTaskQueue.cs
index c08a1d4..30032bb 100644
--- a/Tapeti/Tasks/SingleThreadTaskQueue.cs
+++ b/Tapeti/Tasks/SingleThreadTaskQueue.cs
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
namespace Tapeti.Tasks
{
- ///
///
/// An implementation of a queue which runs tasks on a single thread.
///
@@ -121,7 +120,7 @@ namespace Tapeti.Tasks
{
while (true)
{
- Task task;
+ Task? task;
lock (scheduledTasks)
{
task = WaitAndDequeueTask();
@@ -134,7 +133,7 @@ namespace Tapeti.Tasks
}
}
- private Task WaitAndDequeueTask()
+ private Task? WaitAndDequeueTask()
{
while (!scheduledTasks.Any() && !disposed)
Monitor.Wait(scheduledTasks);
diff --git a/appveyor.yml b/appveyor.yml
index 1026791..023ba38 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-image: Visual Studio 2019
+image: Visual Studio 2022
install:
@@ -9,19 +9,21 @@ before_build:
- ps: gitversion /l console /output buildserver
- ps: build\UpdateVersion.ps1
+environment:
+ pack_params: -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output -p:Configuration=Release -p:ContinuousIntegrationBuild=true
+
after_build:
# Create NuGet packages
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti\Tapeti.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Flow\Tapeti.Flow.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Transient\Tapeti.Transient.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Serilog\Tapeti.Serilog.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Autofac\Tapeti.Autofac.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.Ninject\Tapeti.Ninject.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
- - cmd: dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:PublishRepositoryUrl=true -p:EmbedUntrackedSources=true --output output Tapeti.UnityContainer\Tapeti.UnityContainer.csproj /p:Configuration=Release /p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti\Tapeti.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Flow\Tapeti.Flow.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Transient\Tapeti.Transient.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Serilog\Tapeti.Serilog.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Autofac\Tapeti.Autofac.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
+ - cmd: dotnet pack Tapeti.Ninject\Tapeti.Ninject.csproj %pack_params% -p:Version=%GitVersion_NuGetVersion%
# Push artifacts
- ps: Get-ChildItem output\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
- ps: Get-ChildItem output\*.snupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
@@ -29,6 +31,10 @@ after_build:
build:
project: Tapeti.sln
+test_script:
+ - dotnet test Tapeti.Tests\bin\Release\net6.0\Tapeti.Tests.dll --filter "Category!=Requires Docker"
+ - dotnet test Tapeti.Tests\bin\Release\net7.0\Tapeti.Tests.dll --filter "Category!=Requires Docker"
+
platform:
- Any CPU