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 GetMiddleware(IDependencyResolver dependencyResolver) { - return null; + return Enumerable.Empty(); } } } diff --git a/Tapeti.Flow.SQL/SqlConnectionFlowRepository.cs b/Tapeti.Flow.SQL/SqlConnectionFlowRepository.cs index 3da64b3..3cc02af 100644 --- a/Tapeti.Flow.SQL/SqlConnectionFlowRepository.cs +++ b/Tapeti.Flow.SQL/SqlConnectionFlowRepository.cs @@ -5,9 +5,12 @@ using System.Data.SqlClient; using System.Threading.Tasks; using Newtonsoft.Json; +// Neither of these are available in language version 7 required for .NET Standard 2.0 +// ReSharper disable ConvertToUsingDeclaration +// ReSharper disable UseAwaitUsing + namespace Tapeti.Flow.SQL { - /// /// /// IFlowRepository implementation for SQL server. /// @@ -37,7 +40,7 @@ namespace Tapeti.Flow.SQL /// - public async Task>> GetStates() + public async ValueTask>> GetStates() { return await SqlRetryHelper.Execute(async () => { @@ -55,7 +58,8 @@ namespace Tapeti.Flow.SQL var stateJson = flowReader.GetString(2); var state = JsonConvert.DeserializeObject(stateJson); - result.Add(new FlowRecord(flowID, creationTime, state)); + if (state != null) + result.Add(new FlowRecord(flowID, creationTime, state)); } return result; @@ -64,7 +68,7 @@ namespace Tapeti.Flow.SQL } /// - public async Task CreateState(Guid flowID, T state, DateTime timestamp) + public async ValueTask CreateState(Guid flowID, T state, DateTime timestamp) { await SqlRetryHelper.Execute(async () => { @@ -88,7 +92,7 @@ namespace Tapeti.Flow.SQL } /// - public async Task UpdateState(Guid flowID, T state) + public async ValueTask UpdateState(Guid flowID, T state) { await SqlRetryHelper.Execute(async () => { @@ -108,7 +112,7 @@ namespace Tapeti.Flow.SQL } /// - public async Task DeleteState(Guid flowID) + public async ValueTask DeleteState(Guid flowID) { await SqlRetryHelper.Execute(async () => { diff --git a/Tapeti.Flow.SQL/SqlExceptionHelper.cs b/Tapeti.Flow.SQL/SqlExceptionHelper.cs index e4d507e..dc00d31 100644 --- a/Tapeti.Flow.SQL/SqlExceptionHelper.cs +++ b/Tapeti.Flow.SQL/SqlExceptionHelper.cs @@ -13,7 +13,7 @@ namespace Tapeti.Flow.SQL // 2627: Violation of %ls constraint '%.*ls'. Cannot insert duplicate key in object '%.*ls'. The duplicate key value is %ls. public static bool IsDuplicateKey(SqlException e) { - return e != null && (e.Number == 2601 || e.Number == 2627); + return e is { Number: 2601 or 2627 }; } @@ -21,12 +21,12 @@ namespace Tapeti.Flow.SQL { switch (e) { - case TimeoutException _: + case TimeoutException: return true; - case Exception exception: + case not null: { - var sqlExceptions = ExtractSqlExceptions(exception); + var sqlExceptions = ExtractSqlExceptions(e); return sqlExceptions.Select(UnwrapSqlErrors).Any(IsRecoverableSQLError); } @@ -38,11 +38,13 @@ namespace Tapeti.Flow.SQL /// /// Extracts alls SqlExceptions from the main and inner or aggregate exceptions /// - public static IEnumerable ExtractSqlExceptions(Exception e) + public static IEnumerable ExtractSqlExceptions(Exception exception) { - while (e != null) + var exceptionHead = exception; + + while (exceptionHead != null) { - switch (e) + switch (exceptionHead) { case AggregateException aggregateException: foreach (var innerException in aggregateException.InnerExceptions) @@ -56,7 +58,8 @@ namespace Tapeti.Flow.SQL yield return sqlException; break; } - e = e.InnerException; + + exceptionHead = exceptionHead.InnerException; } } @@ -66,12 +69,14 @@ namespace Tapeti.Flow.SQL /// public static IEnumerable UnwrapSqlErrors(SqlException exception) { - while (exception != null) + var exceptionHead = exception; + + while (exceptionHead != null) { - foreach (SqlError error in exception.Errors) + foreach (SqlError error in exceptionHead.Errors) yield return error; - exception = exception.InnerException as SqlException; + exceptionHead = exceptionHead.InnerException as SqlException; } } diff --git a/Tapeti.Flow.SQL/SqlRetryHelper.cs b/Tapeti.Flow.SQL/SqlRetryHelper.cs index 51068d7..5db5b60 100644 --- a/Tapeti.Flow.SQL/SqlRetryHelper.cs +++ b/Tapeti.Flow.SQL/SqlRetryHelper.cs @@ -54,7 +54,7 @@ namespace Tapeti.Flow.SQL returnValue = await callback(); }); - return returnValue; + return returnValue!; } } } diff --git a/Tapeti.Flow.SQL/Tapeti.Flow.SQL.csproj b/Tapeti.Flow.SQL/Tapeti.Flow.SQL.csproj index f3aaee5..0a4ca77 100644 --- a/Tapeti.Flow.SQL/Tapeti.Flow.SQL.csproj +++ b/Tapeti.Flow.SQL/Tapeti.Flow.SQL.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net6.0;net7.0 true Mark van Renswoude @@ -11,12 +11,19 @@ https://github.com/MvRens/Tapeti Tapeti.Flow.SQL.png 2.0.0 + 9 + enable 1701;1702 + + + IDE0063 + + @@ -26,7 +33,7 @@ - + @@ -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