diff --git a/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj b/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj new file mode 100644 index 0000000..7efcb1a --- /dev/null +++ b/06-StatelessRequestResponse/06-StatelessRequestResponse.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp2.1 + _06_StatelessRequestResponse + + + + + + + + + + + + + + diff --git a/06-StatelessRequestResponse/ExampleMessageController.cs b/06-StatelessRequestResponse/ExampleMessageController.cs new file mode 100644 index 0000000..fe01f2e --- /dev/null +++ b/06-StatelessRequestResponse/ExampleMessageController.cs @@ -0,0 +1,28 @@ +using System; +using ExampleLib; +using Messaging.TapetiExample; +using Tapeti.Annotations; + +namespace _06_StatelessRequestResponse +{ + [MessageController] + [DynamicQueue("tapeti.example.06")] + public class ExampleMessageController + { + private readonly IExampleState exampleState; + + + public ExampleMessageController(IExampleState exampleState) + { + this.exampleState = exampleState; + } + + + [ResponseHandler] + public void HandleQuoteResponse(QuoteResponseMessage message) + { + Console.WriteLine("Received response: " + message.Quote); + exampleState.Done(); + } + } +} diff --git a/06-StatelessRequestResponse/Program.cs b/06-StatelessRequestResponse/Program.cs new file mode 100644 index 0000000..615cb9b --- /dev/null +++ b/06-StatelessRequestResponse/Program.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using ExampleLib; +using Messaging.TapetiExample; +using SimpleInjector; +using Tapeti; +using Tapeti.DataAnnotations; +using Tapeti.Default; +using Tapeti.SimpleInjector; + +namespace _06_StatelessRequestResponse +{ + public class Program + { + public static void Main(string[] args) + { + var container = new Container(); + var dependencyResolver = new SimpleInjectorDependencyResolver(container); + + container.Register(); + + var helper = new ExampleConsoleApp(dependencyResolver); + helper.Run(MainAsync); + } + + + internal static async Task MainAsync(IDependencyResolver dependencyResolver, Func waitForDone) + { + var config = new TapetiConfig(dependencyResolver) + .WithDataAnnotations() + .RegisterAllControllers() + .Build(); + + + using (var connection = new TapetiConnection(config)) + { + await connection.Subscribe(); + + var publisher = dependencyResolver.Resolve(); + await publisher.PublishRequest( + new QuoteRequestMessage + { + Amount = 1 + }, + c => c.HandleQuoteResponse); + + await waitForDone(); + } + } + } +} diff --git a/06-StatelessRequestResponse/ReceivingMessageController.cs b/06-StatelessRequestResponse/ReceivingMessageController.cs new file mode 100644 index 0000000..6684780 --- /dev/null +++ b/06-StatelessRequestResponse/ReceivingMessageController.cs @@ -0,0 +1,38 @@ +using Messaging.TapetiExample; +using Tapeti.Annotations; + +namespace _06_StatelessRequestResponse +{ + [MessageController] + [DynamicQueue("tapeti.example.06.receiver")] + public class ReceivingMessageController + { + // No publisher required, responses can simply be returned + public QuoteResponseMessage HandleQuoteRequest(QuoteRequestMessage message) + { + string quote; + + switch (message.Amount) + { + case 1: + // Well, they asked for it... :-) + quote = "'"; + break; + + case 2: + quote = "\""; + break; + + default: + // We have to return a response. + quote = null; + break; + } + + return new QuoteResponseMessage + { + Quote = quote + }; + } + } +} diff --git a/Tapeti.Annotations/ResponseHandlerAttribute.cs b/Tapeti.Annotations/ResponseHandlerAttribute.cs new file mode 100644 index 0000000..613857f --- /dev/null +++ b/Tapeti.Annotations/ResponseHandlerAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Tapeti.Annotations +{ + /// + /// + /// Indicates that the method only handles response messages which are sent directly + /// to the queue. No binding will be created. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ResponseHandlerAttribute : Attribute + { + } +} diff --git a/Tapeti.Cmd/Tapeti.Cmd.csproj b/Tapeti.Cmd/Tapeti.Cmd.csproj index 6a80ffb..01626c4 100644 --- a/Tapeti.Cmd/Tapeti.Cmd.csproj +++ b/Tapeti.Cmd/Tapeti.Cmd.csproj @@ -1,18 +1,18 @@ - - - - Exe - netcoreapp2.2 - 2.0.0 - Mark van Renswoude - Mark van Renswoude - Tapeti Command-line Utility - - - - - - - - - + + + + Exe + netcoreapp2.1 + 2.0.0 + Mark van Renswoude + Mark van Renswoude + Tapeti Command-line Utility + + + + + + + + + diff --git a/Tapeti.Serilog/TapetiSeriLogger.cs b/Tapeti.Serilog/TapetiSeriLogger.cs index af5690b..1c2118a 100644 --- a/Tapeti.Serilog/TapetiSeriLogger.cs +++ b/Tapeti.Serilog/TapetiSeriLogger.cs @@ -6,16 +6,18 @@ using ISerilogLogger = Serilog.ILogger; namespace Tapeti.Serilog { - /// /// /// Implements the Tapeti ILogger interface for Serilog output. /// - public class TapetiSeriLogger: ILogger + public class TapetiSeriLogger: IBindingLogger { private readonly ISerilogLogger seriLogger; - /// + /// + /// Create a Tapeti ILogger implementation to output to the specified Serilog.ILogger interface + /// + /// The Serilog.ILogger implementation to output Tapeti log message to public TapetiSeriLogger(ISerilogLogger seriLogger) { this.seriLogger = seriLogger; @@ -82,6 +84,39 @@ namespace Tapeti.Serilog contextLogger.Error(exception, "Tapeti: exception in message handler"); } + /// + public void QueueDeclare(string queueName, bool durable, bool passive) + { + if (passive) + seriLogger.Information("Tapeti: verifying durable queue {queueName}", queueName); + else + seriLogger.Information("Tapeti: declaring {queueType} queue {queueName}", durable ? "durable" : "dynamic", queueName); + } + + /// + public void QueueBind(string queueName, bool durable, string exchange, string routingKey) + { + seriLogger.Information("Tapeti: binding {queueName} to exchange {exchange} with routing key {routingKey}", + queueName, + exchange, + routingKey); + } + + /// + public void QueueUnbind(string queueName, string exchange, string routingKey) + { + seriLogger.Information("Tapeti: removing binding for {queueName} to exchange {exchange} with routing key {routingKey}", + queueName, + exchange, + routingKey); + } + + /// + public void ExchangeDeclare(string exchange) + { + seriLogger.Information("Tapeti: declaring exchange {exchange}", exchange); + } + /// public void QueueObsolete(string queueName, bool deleted, uint messageCount) { diff --git a/Tapeti.sln b/Tapeti.sln index c3e5cf3..016fd47 100644 --- a/Tapeti.sln +++ b/Tapeti.sln @@ -1,184 +1,191 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2026 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Annotations", "Tapeti.Annotations\Tapeti.Annotations.csproj", "{4B742AB2-59DD-4792-8E0F-D80B5366B844}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations", "Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj", "{6504D430-AB4A-4DE3-AE76-0384591BEEE7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow", "Tapeti.Flow\Tapeti.Flow.csproj", "{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow.SQL", "Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj", "{775CAB72-F443-442E-8E10-313B2548EDF8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.SimpleInjector", "Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj", "{A190C736-E95A-4BDA-AA80-6211226DFCAD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Tests", "Tapeti.Tests\Tapeti.Tests.csproj", "{334F3715-63CF-4D13-B09A-38E2A616D4F5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Serilog", "Tapeti.Serilog\Tapeti.Serilog.csproj", "{43AA5DF3-49D5-4795-A290-D6511502B564}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Transient", "Tapeti.Transient\Tapeti.Transient.csproj", "{A6355E63-19AB-47EA-91FA-49B5E9B41F88}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations.Extensions", "Tapeti.DataAnnotations.Extensions\Tapeti.DataAnnotations.Extensions.csproj", "{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{266B9B94-A4D2-41C2-860C-24A7C3B63B56}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01-PublishSubscribe", "Examples\01-PublishSubscribe\01-PublishSubscribe.csproj", "{8350A0AB-F0EE-48CF-9CA6-6019467101CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleLib", "Examples\ExampleLib\ExampleLib.csproj", "{F3B38753-06B4-4932-84B4-A07692AD802D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.TapetiExample", "Examples\Messaging.TapetiExample\Messaging.TapetiExample.csproj", "{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02-DeclareDurableQueues", "Examples\02-DeclareDurableQueues\02-DeclareDurableQueues.csproj", "{85511282-EF91-4B56-B7DC-9E8706556D6E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03-FlowRequestResponse", "Examples\03-FlowRequestResponse\03-FlowRequestResponse.csproj", "{463A12CE-E221-450D-ADEA-91A599612DFA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "04-Transient", "Examples\04-Transient\04-Transient.csproj", "{46DFC131-A398-435F-A7DF-3C41B656BF11}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "05-SpeedTest", "Examples\05-SpeedTest\05-SpeedTest.csproj", "{330D05CE-5321-4C7D-8017-2070B891289E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoC", "IoC", "{99380F97-AD1A-459F-8AB3-D404E1E6AD4F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8E757FF7-F6D7-42B1-827F-26FA95D97803}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{57996ADC-18C5-4991-9F95-58D58D442461}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.CastleWindsor", "Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj", "{374AAE64-598B-4F67-8870-4A05168FF987}" -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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{62002327-46B0-4B72-B95A-594CE7F8C80D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Cmd", "Tapeti.Cmd\Tapeti.Cmd.csproj", "{C8728BFC-7F97-41BC-956B-690A57B634EC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.Build.0 = Release|Any CPU - {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.Build.0 = Release|Any CPU - {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.Build.0 = Release|Any CPU - {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.Build.0 = Release|Any CPU - {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.Build.0 = Release|Any CPU - {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.Build.0 = Release|Any CPU - {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.Build.0 = Release|Any CPU - {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.Build.0 = Release|Any CPU - {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.Build.0 = Release|Any CPU - {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.Build.0 = Release|Any CPU - {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.Build.0 = Release|Any CPU - {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.Build.0 = Release|Any CPU - {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.Build.0 = Release|Any CPU - {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.Build.0 = Release|Any CPU - {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.Build.0 = Release|Any CPU - {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.Build.0 = Release|Any CPU - {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.Build.0 = Release|Any CPU - {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.Build.0 = Debug|Any CPU - {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.ActiveCfg = Release|Any CPU - {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.Build.0 = Release|Any CPU - {B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - {29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.Build.0 = Release|Any CPU - {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {4B742AB2-59DD-4792-8E0F-D80B5366B844} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} - {2952B141-C54D-4E6F-8108-CAD735B0279F} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} - {6504D430-AB4A-4DE3-AE76-0384591BEEE7} = {57996ADC-18C5-4991-9F95-58D58D442461} - {14CF8F01-570B-4B84-AB4A-E0C3EC117F89} = {57996ADC-18C5-4991-9F95-58D58D442461} - {775CAB72-F443-442E-8E10-313B2548EDF8} = {57996ADC-18C5-4991-9F95-58D58D442461} - {A190C736-E95A-4BDA-AA80-6211226DFCAD} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} - {43AA5DF3-49D5-4795-A290-D6511502B564} = {57996ADC-18C5-4991-9F95-58D58D442461} - {A6355E63-19AB-47EA-91FA-49B5E9B41F88} = {57996ADC-18C5-4991-9F95-58D58D442461} - {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831} = {57996ADC-18C5-4991-9F95-58D58D442461} - {8350A0AB-F0EE-48CF-9CA6-6019467101CF} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {F3B38753-06B4-4932-84B4-A07692AD802D} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {85511282-EF91-4B56-B7DC-9E8706556D6E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {463A12CE-E221-450D-ADEA-91A599612DFA} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {46DFC131-A398-435F-A7DF-3C41B656BF11} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} - {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} - {C8728BFC-7F97-41BC-956B-690A57B634EC} = {62002327-46B0-4B72-B95A-594CE7F8C80D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B09CC2BF-B2AF-4CB6-8728-5D1D8E5C50FA} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2026 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Annotations", "Tapeti.Annotations\Tapeti.Annotations.csproj", "{4B742AB2-59DD-4792-8E0F-D80B5366B844}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations", "Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj", "{6504D430-AB4A-4DE3-AE76-0384591BEEE7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow", "Tapeti.Flow\Tapeti.Flow.csproj", "{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow.SQL", "Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj", "{775CAB72-F443-442E-8E10-313B2548EDF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.SimpleInjector", "Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj", "{A190C736-E95A-4BDA-AA80-6211226DFCAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Tests", "Tapeti.Tests\Tapeti.Tests.csproj", "{334F3715-63CF-4D13-B09A-38E2A616D4F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Serilog", "Tapeti.Serilog\Tapeti.Serilog.csproj", "{43AA5DF3-49D5-4795-A290-D6511502B564}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Transient", "Tapeti.Transient\Tapeti.Transient.csproj", "{A6355E63-19AB-47EA-91FA-49B5E9B41F88}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations.Extensions", "Tapeti.DataAnnotations.Extensions\Tapeti.DataAnnotations.Extensions.csproj", "{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{266B9B94-A4D2-41C2-860C-24A7C3B63B56}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01-PublishSubscribe", "Examples\01-PublishSubscribe\01-PublishSubscribe.csproj", "{8350A0AB-F0EE-48CF-9CA6-6019467101CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleLib", "Examples\ExampleLib\ExampleLib.csproj", "{F3B38753-06B4-4932-84B4-A07692AD802D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.TapetiExample", "Examples\Messaging.TapetiExample\Messaging.TapetiExample.csproj", "{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02-DeclareDurableQueues", "Examples\02-DeclareDurableQueues\02-DeclareDurableQueues.csproj", "{85511282-EF91-4B56-B7DC-9E8706556D6E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03-FlowRequestResponse", "Examples\03-FlowRequestResponse\03-FlowRequestResponse.csproj", "{463A12CE-E221-450D-ADEA-91A599612DFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "04-Transient", "Examples\04-Transient\04-Transient.csproj", "{46DFC131-A398-435F-A7DF-3C41B656BF11}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "05-SpeedTest", "Examples\05-SpeedTest\05-SpeedTest.csproj", "{330D05CE-5321-4C7D-8017-2070B891289E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoC", "IoC", "{99380F97-AD1A-459F-8AB3-D404E1E6AD4F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8E757FF7-F6D7-42B1-827F-26FA95D97803}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{57996ADC-18C5-4991-9F95-58D58D442461}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.CastleWindsor", "Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj", "{374AAE64-598B-4F67-8870-4A05168FF987}" +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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{62002327-46B0-4B72-B95A-594CE7F8C80D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Cmd", "Tapeti.Cmd\Tapeti.Cmd.csproj", "{C8728BFC-7F97-41BC-956B-690A57B634EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "06-StatelessRequestResponse", "06-StatelessRequestResponse\06-StatelessRequestResponse.csproj", "{152227AA-3165-4550-8997-6EA80C84516E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.Build.0 = Release|Any CPU + {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.Build.0 = Release|Any CPU + {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.Build.0 = Release|Any CPU + {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.Build.0 = Release|Any CPU + {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.Build.0 = Release|Any CPU + {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.Build.0 = Release|Any CPU + {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.Build.0 = Release|Any CPU + {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.Build.0 = Release|Any CPU + {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.Build.0 = Release|Any CPU + {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.Build.0 = Release|Any CPU + {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.Build.0 = Release|Any CPU + {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.Build.0 = Release|Any CPU + {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.Build.0 = Release|Any CPU + {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.Build.0 = Release|Any CPU + {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.Build.0 = Release|Any CPU + {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.Build.0 = Debug|Any CPU + {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.ActiveCfg = Release|Any CPU + {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.Build.0 = Release|Any CPU + {B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + {29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.Build.0 = Release|Any CPU + {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.Build.0 = Release|Any CPU + {152227AA-3165-4550-8997-6EA80C84516E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {152227AA-3165-4550-8997-6EA80C84516E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {152227AA-3165-4550-8997-6EA80C84516E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {152227AA-3165-4550-8997-6EA80C84516E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4B742AB2-59DD-4792-8E0F-D80B5366B844} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} + {2952B141-C54D-4E6F-8108-CAD735B0279F} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} + {6504D430-AB4A-4DE3-AE76-0384591BEEE7} = {57996ADC-18C5-4991-9F95-58D58D442461} + {14CF8F01-570B-4B84-AB4A-E0C3EC117F89} = {57996ADC-18C5-4991-9F95-58D58D442461} + {775CAB72-F443-442E-8E10-313B2548EDF8} = {57996ADC-18C5-4991-9F95-58D58D442461} + {A190C736-E95A-4BDA-AA80-6211226DFCAD} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} + {43AA5DF3-49D5-4795-A290-D6511502B564} = {57996ADC-18C5-4991-9F95-58D58D442461} + {A6355E63-19AB-47EA-91FA-49B5E9B41F88} = {57996ADC-18C5-4991-9F95-58D58D442461} + {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831} = {57996ADC-18C5-4991-9F95-58D58D442461} + {8350A0AB-F0EE-48CF-9CA6-6019467101CF} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {F3B38753-06B4-4932-84B4-A07692AD802D} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {85511282-EF91-4B56-B7DC-9E8706556D6E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {463A12CE-E221-450D-ADEA-91A599612DFA} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {46DFC131-A398-435F-A7DF-3C41B656BF11} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + {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} + {C8728BFC-7F97-41BC-956B-690A57B634EC} = {62002327-46B0-4B72-B95A-594CE7F8C80D} + {152227AA-3165-4550-8997-6EA80C84516E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B09CC2BF-B2AF-4CB6-8728-5D1D8E5C50FA} + EndGlobalSection +EndGlobal diff --git a/Tapeti/Config/ITapetiConfig.cs b/Tapeti/Config/ITapetiConfig.cs index b1108f7..515ea0e 100644 --- a/Tapeti/Config/ITapetiConfig.cs +++ b/Tapeti/Config/ITapetiConfig.cs @@ -1,112 +1,120 @@ -using System; -using System.Collections.Generic; - -namespace Tapeti.Config -{ - /// - /// Provides access to the Tapeti configuration. - /// - public interface ITapetiConfig - { - /// - /// Reference to the wrapper for an IoC container, to provide dependency injection to Tapeti. - /// - IDependencyResolver DependencyResolver { get; } - - /// - /// Various Tapeti features which can be turned on or off. - /// - ITapetiConfigFeatues Features { get; } - - /// - /// Provides access to the different kinds of registered middleware. - /// - ITapetiConfigMiddleware Middleware { get; } - - /// - /// A list of all registered bindings. - /// - ITapetiConfigBindings Bindings { get; } - } - - - /// - /// Various Tapeti features which can be turned on or off. - /// - public interface ITapetiConfigFeatues - { - /// - /// Determines whether 'publisher confirms' are used. This RabbitMQ features allows Tapeti to - /// be notified if a message has no route, and guarantees delivery for request-response style - /// messages and those marked with the Mandatory attribute. On by default, can only be turned - /// off by explicitly calling DisablePublisherConfirms, which is not recommended. - /// - bool PublisherConfirms { get; } - - /// - /// If enabled, durable queues will be created at startup and their bindings will be updated - /// with the currently registered message handlers. If not enabled all durable queues must - /// already be present when the connection is made. - /// - bool DeclareDurableQueues { get; } - } - - - /// - /// Provides access to the different kinds of registered middleware. - /// - public interface ITapetiConfigMiddleware - { - /// - /// A list of message middleware which is called when a message is being consumed. - /// - IReadOnlyList Message { get; } - - /// - /// A list of publish middleware which is called when a message is being published. - /// - IReadOnlyList Publish { get; } - } - - - /// - /// - /// Contains a list of registered bindings, with a few added helpers. - /// - public interface ITapetiConfigBindings : IReadOnlyList - { - /// - /// Searches for a binding linked to the specified method. - /// - /// - /// The binding if found, null otherwise - IControllerMethodBinding ForMethod(Delegate 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); - } - */ -} +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Tapeti.Config +{ + /// + /// Provides access to the Tapeti configuration. + /// + public interface ITapetiConfig + { + /// + /// Reference to the wrapper for an IoC container, to provide dependency injection to Tapeti. + /// + IDependencyResolver DependencyResolver { get; } + + /// + /// Various Tapeti features which can be turned on or off. + /// + ITapetiConfigFeatues Features { get; } + + /// + /// Provides access to the different kinds of registered middleware. + /// + ITapetiConfigMiddleware Middleware { get; } + + /// + /// A list of all registered bindings. + /// + ITapetiConfigBindings Bindings { get; } + } + + + /// + /// Various Tapeti features which can be turned on or off. + /// + public interface ITapetiConfigFeatues + { + /// + /// Determines whether 'publisher confirms' are used. This RabbitMQ features allows Tapeti to + /// be notified if a message has no route, and guarantees delivery for request-response style + /// messages and those marked with the Mandatory attribute. On by default, can only be turned + /// off by explicitly calling DisablePublisherConfirms, which is not recommended. + /// + bool PublisherConfirms { get; } + + /// + /// If enabled, durable queues will be created at startup and their bindings will be updated + /// with the currently registered message handlers. If not enabled all durable queues must + /// already be present when the connection is made. + /// + bool DeclareDurableQueues { get; } + } + + + /// + /// Provides access to the different kinds of registered middleware. + /// + public interface ITapetiConfigMiddleware + { + /// + /// A list of message middleware which is called when a message is being consumed. + /// + IReadOnlyList Message { get; } + + /// + /// A list of publish middleware which is called when a message is being published. + /// + IReadOnlyList Publish { get; } + } + + + /// + /// + /// Contains a list of registered bindings, with a few added helpers. + /// + public interface ITapetiConfigBindings : IReadOnlyList + { + /// + /// Searches for a binding linked to the specified method. + /// + /// + /// The binding if found, null otherwise + IControllerMethodBinding ForMethod(Delegate method); + + /// + /// Searches for a binding linked to the specified method. + /// + /// + /// The binding if found, null otherwise + 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/Connection/TapetiClient.cs b/Tapeti/Connection/TapetiClient.cs index 9a91ed0..eb04704 100644 --- a/Tapeti/Connection/TapetiClient.cs +++ b/Tapeti/Connection/TapetiClient.cs @@ -237,22 +237,29 @@ namespace Tapeti.Connection { var existingBindings = (await GetQueueBindings(queueName)).ToList(); var currentBindings = bindings.ToList(); + var bindingLogger = logger as IBindingLogger; await Queue(channel => { if (cancellationToken.IsCancellationRequested) return; + bindingLogger?.QueueDeclare(queueName, true, false); channel.QueueDeclare(queueName, true, false, false); + foreach (var binding in currentBindings.Except(existingBindings)) { DeclareExchange(channel, binding.Exchange); + bindingLogger?.QueueBind(queueName, true, binding.Exchange, binding.RoutingKey); channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey); } foreach (var deletedBinding in existingBindings.Except(currentBindings)) + { + bindingLogger?.QueueUnbind(queueName, deletedBinding.Exchange, deletedBinding.RoutingKey); channel.QueueUnbind(queueName, deletedBinding.Exchange, deletedBinding.RoutingKey); + } }); } @@ -264,6 +271,7 @@ namespace Tapeti.Connection if (cancellationToken.IsCancellationRequested) return; + (logger as IBindingLogger)?.QueueDeclare(queueName, true, true); channel.QueueDeclarePassive(queueName); }); } @@ -285,7 +293,7 @@ namespace Tapeti.Connection }); deletedQueues.Add(queueName); - logger.QueueObsolete(queueName, true, deletedMessages); + (logger as IBindingLogger)?.QueueObsolete(queueName, true, deletedMessages); return; } @@ -321,7 +329,7 @@ namespace Tapeti.Connection channel.QueueDelete(queueName, false, true); deletedQueues.Add(queueName); - logger.QueueObsolete(queueName, true, 0); + (logger as IBindingLogger)?.QueueObsolete(queueName, true, 0); } catch (OperationInterruptedException e) { @@ -344,7 +352,7 @@ namespace Tapeti.Connection channel.QueueUnbind(queueName, binding.Exchange, binding.RoutingKey); } - logger.QueueObsolete(queueName, false, queueInfo.Messages); + (logger as IBindingLogger)?.QueueObsolete(queueName, false, queueInfo.Messages); } } while (retry); }); @@ -355,6 +363,7 @@ namespace Tapeti.Connection public async Task DynamicQueueDeclare(CancellationToken cancellationToken, string queuePrefix = null) { string queueName = null; + var bindingLogger = logger as IBindingLogger; await Queue(channel => { @@ -364,10 +373,14 @@ namespace Tapeti.Connection if (!string.IsNullOrEmpty(queuePrefix)) { queueName = queuePrefix + "." + Guid.NewGuid().ToString("N"); + bindingLogger?.QueueDeclare(queueName, false, false); channel.QueueDeclare(queueName); } else + { queueName = channel.QueueDeclare().QueueName; + bindingLogger?.QueueDeclare(queueName, false, false); + } }); return queueName; @@ -381,8 +394,9 @@ namespace Tapeti.Connection if (cancellationToken.IsCancellationRequested) return; - DeclareExchange(channel, binding.Exchange); - channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey); + DeclareExchange(channel, binding.Exchange); + (logger as IBindingLogger)?.QueueBind(queueName, false, binding.Exchange, binding.RoutingKey); + channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey); }); } @@ -554,6 +568,7 @@ namespace Tapeti.Connection if (declaredExchanges.Contains(exchange)) return; + (logger as IBindingLogger)?.ExchangeDeclare(exchange); channel.ExchangeDeclare(exchange, "topic", true); declaredExchanges.Add(exchange); } diff --git a/Tapeti/Connection/TapetiPublisher.cs b/Tapeti/Connection/TapetiPublisher.cs index 53f0077..9955aa4 100644 --- a/Tapeti/Connection/TapetiPublisher.cs +++ b/Tapeti/Connection/TapetiPublisher.cs @@ -1,104 +1,163 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; -using Tapeti.Annotations; -using Tapeti.Config; -using Tapeti.Default; -using Tapeti.Helpers; - -namespace Tapeti.Connection -{ - /// - internal class TapetiPublisher : IInternalPublisher - { - private readonly ITapetiConfig config; - private readonly Func clientFactory; - private readonly IExchangeStrategy exchangeStrategy; - private readonly IRoutingKeyStrategy routingKeyStrategy; - private readonly IMessageSerializer messageSerializer; - - - /// - public TapetiPublisher(ITapetiConfig config, Func clientFactory) - { - this.config = config; - this.clientFactory = clientFactory; - - exchangeStrategy = config.DependencyResolver.Resolve(); - routingKeyStrategy = config.DependencyResolver.Resolve(); - messageSerializer = config.DependencyResolver.Resolve(); - } - - - /// - public async Task Publish(object message) - { - await Publish(message, null, IsMandatory(message)); - } - - - /// - public async Task Publish(object message, IMessageProperties properties, bool mandatory) - { - var messageClass = message.GetType(); - var exchange = exchangeStrategy.GetExchange(messageClass); - var routingKey = routingKeyStrategy.GetRoutingKey(messageClass); - - await Publish(message, properties, exchange, routingKey, 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) - { - var writableProperties = new MessageProperties(properties); - - if (!writableProperties.Timestamp.HasValue) - writableProperties.Timestamp = DateTime.UtcNow; - - writableProperties.Persistent = true; - - - var context = new PublishContext - { - Config = config, - Exchange = exchange, - RoutingKey = routingKey, - Message = message, - Properties = writableProperties - }; - - - await MiddlewareHelper.GoAsync( - config.Middleware.Publish, - async (handler, next) => await handler.Handle(context, next), - async () => - { - var body = messageSerializer.Serialize(message, writableProperties); - await clientFactory().Publish(body, writableProperties, exchange, routingKey, mandatory); - }); - } - - - private static bool IsMandatory(object message) - { - return message.GetType().GetCustomAttribute() != null; - } - - - 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; } - } - } -} +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Tapeti.Annotations; +using Tapeti.Config; +using Tapeti.Default; +using Tapeti.Helpers; + +namespace Tapeti.Connection +{ + /// + internal class TapetiPublisher : IInternalPublisher + { + private readonly ITapetiConfig config; + private readonly Func clientFactory; + private readonly IExchangeStrategy exchangeStrategy; + private readonly IRoutingKeyStrategy routingKeyStrategy; + private readonly IMessageSerializer messageSerializer; + + + public TapetiPublisher(ITapetiConfig config, Func clientFactory) + { + this.config = config; + this.clientFactory = clientFactory; + + exchangeStrategy = config.DependencyResolver.Resolve(); + routingKeyStrategy = config.DependencyResolver.Resolve(); + messageSerializer = config.DependencyResolver.Resolve(); + } + + + /// + public async Task Publish(object message) + { + await Publish(message, null, IsMandatory(message)); + } + + + /// + public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class + { + await PublishRequest(message, responseMethodSelector.Body); + } + + + /// + public async Task PublishRequest(TRequest message, Expression>> responseMethodSelector) where TController : class + { + await PublishRequest(message, responseMethodSelector.Body); + } + + + private async Task PublishRequest(object message, Expression responseMethodBody) + { + var callExpression = (responseMethodBody as UnaryExpression)?.Operand as MethodCallExpression; + var targetMethodExpression = callExpression?.Object as ConstantExpression; + + var responseHandler = targetMethodExpression?.Value as MethodInfo; + if (responseHandler == null) + throw new ArgumentException("Unable to determine the response method", nameof(responseMethodBody)); + + + var requestAttribute = message.GetType().GetCustomAttribute(); + if (requestAttribute?.Response == null) + throw new ArgumentException($"Request message {message.GetType().Name} must be marked with the Request attribute and a valid Response type", nameof(message)); + + var binding = config.Bindings.ForMethod(responseHandler); + if (binding == null) + throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler)); + + if (!binding.Accept(requestAttribute.Response)) + throw new ArgumentException($"responseHandler must accept message of type {requestAttribute.Response}", nameof(responseHandler)); + + var responseHandleAttribute = binding.Method.GetCustomAttribute(); + if (responseHandleAttribute == null) + throw new ArgumentException("responseHandler must be marked with the ResponseHandler attribute", nameof(responseHandler)); + + if (binding.QueueName == null) + throw new ArgumentException("responseHandler is not yet subscribed to a queue, TapetiConnection.Subscribe must be called before starting a request", nameof(responseHandler)); + + + var properties = new MessageProperties + { + ReplyTo = binding.QueueName + }; + + await Publish(message, properties, IsMandatory(message)); + } + + + /// + public async Task SendToQueue(string queueName, object message) + { + await PublishDirect(message, queueName, null, IsMandatory(message)); + } + + + /// + public async Task Publish(object message, IMessageProperties properties, bool mandatory) + { + var messageClass = message.GetType(); + var exchange = exchangeStrategy.GetExchange(messageClass); + var routingKey = routingKeyStrategy.GetRoutingKey(messageClass); + + await Publish(message, properties, exchange, routingKey, 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) + { + var writableProperties = new MessageProperties(properties); + + if (!writableProperties.Timestamp.HasValue) + writableProperties.Timestamp = DateTime.UtcNow; + + writableProperties.Persistent = true; + + + var context = new PublishContext + { + Config = config, + Exchange = exchange, + RoutingKey = routingKey, + Message = message, + Properties = writableProperties + }; + + + await MiddlewareHelper.GoAsync( + config.Middleware.Publish, + async (handler, next) => await handler.Handle(context, next), + async () => + { + var body = messageSerializer.Serialize(message, writableProperties); + await clientFactory().Publish(body, writableProperties, exchange, routingKey, mandatory); + }); + } + + + private static bool IsMandatory(object message) + { + return message.GetType().GetCustomAttribute() != null; + } + + + 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; } + } + } +} diff --git a/Tapeti/Default/ConsoleLogger.cs b/Tapeti/Default/ConsoleLogger.cs index e942ae0..024bbac 100644 --- a/Tapeti/Default/ConsoleLogger.cs +++ b/Tapeti/Default/ConsoleLogger.cs @@ -7,7 +7,7 @@ namespace Tapeti.Default /// /// Default ILogger implementation for console applications. /// - public class ConsoleLogger : ILogger + public class ConsoleLogger : IBindingLogger { /// public void Connect(IConnectContext connectContext) @@ -52,6 +52,32 @@ namespace Tapeti.Default Console.WriteLine(exception); } + /// + public void QueueDeclare(string queueName, bool durable, bool passive) + { + Console.WriteLine(passive + ? $"[Tapeti] Declaring {(durable ? "durable" : "dynamic")} queue {queueName}" + : $"[Tapeti] Verifying durable queue {queueName}"); + } + + /// + public void QueueBind(string queueName, bool durable, string exchange, string routingKey) + { + Console.WriteLine($"[Tapeti] Binding {queueName} to exchange {exchange} with routing key {routingKey}"); + } + + /// + public void QueueUnbind(string queueName, string exchange, string routingKey) + { + Console.WriteLine($"[Tapeti] Removing binding for {queueName} to exchange {exchange} with routing key {routingKey}"); + } + + /// + public void ExchangeDeclare(string exchange) + { + Console.WriteLine($"[Tapeti] Declaring exchange {exchange}"); + } + /// public void QueueObsolete(string queueName, bool deleted, uint messageCount) { diff --git a/Tapeti/Default/ControllerBindingContext.cs b/Tapeti/Default/ControllerBindingContext.cs index 9d80452..f86dc36 100644 --- a/Tapeti/Default/ControllerBindingContext.cs +++ b/Tapeti/Default/ControllerBindingContext.cs @@ -1,175 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Tapeti.Config; - -namespace Tapeti.Default -{ - internal class ControllerBindingContext : IControllerBindingContext - { - private BindingTargetMode? bindingTargetMode; - private readonly List middleware = new List(); - private readonly List parameters; - private readonly ControllerBindingResult result; - - /// - /// Determines how the binding target is configured. - /// - public BindingTargetMode BindingTargetMode => bindingTargetMode ?? BindingTargetMode.Default; - - - /// - /// Provides access to the registered middleware for this method. - /// - public IReadOnlyList Middleware => middleware; - - - /// - public Type MessageClass { get; set; } - - /// - public bool HasMessageClass => MessageClass != null; - - /// - public Type Controller { get; set; } - - /// - public MethodInfo Method { get; set; } - - /// - public IReadOnlyList Parameters => parameters; - - /// - public IBindingResult Result => result; - - - /// - public ControllerBindingContext(IEnumerable parameters, ParameterInfo result) - { - this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList(); - - this.result = new ControllerBindingResult(result); - } - - - /// - public void SetMessageClass(Type messageClass) - { - if (HasMessageClass) - throw new InvalidOperationException("SetMessageClass can only be called once"); - - MessageClass = messageClass; - } - - - /// - public void SetBindingTargetMode(BindingTargetMode mode) - { - if (bindingTargetMode.HasValue) - throw new InvalidOperationException("SetBindingTargetMode can only be called once"); - - bindingTargetMode = mode; - } - - - /// - public void Use(IControllerMiddlewareBase handler) - { - middleware.Add(handler); - } - - - /// - /// Returns the configured bindings for the parameters. - /// - public IEnumerable GetParameterHandlers() - { - return parameters.Select(p => p.Binding); - } - - - /// - /// Returns the configured result handler. - /// - /// - public ResultHandler GetResultHandler() - { - return result.Handler; - } - } - - - /// - /// - /// Default implementation for IBindingParameter - /// - public class ControllerBindingParameter : IBindingParameter - { - /// - /// Provides access to the configured binding. - /// - public ValueFactory Binding { get; set; } - - - /// - public ParameterInfo Info { get; } - - /// - public bool HasBinding => Binding != null; - - - /// - public ControllerBindingParameter(ParameterInfo info) - { - Info = info; - } - - - /// - public void SetBinding(ValueFactory valueFactory) - { - if (Binding != null) - throw new InvalidOperationException("SetBinding can only be called once"); - - Binding = valueFactory; - } - } - - - /// - /// - /// Default implementation for IBindingResult - /// - public class ControllerBindingResult : IBindingResult - { - /// - /// Provides access to the configured handler. - /// - public ResultHandler Handler { get; set; } - - - /// - public ParameterInfo Info { get; } - - /// - public bool HasHandler => Handler != null; - - - /// - public ControllerBindingResult(ParameterInfo info) - { - Info = info; - } - - - /// - public void SetHandler(ResultHandler resultHandler) - { - if (Handler != null) - throw new InvalidOperationException("SetHandler can only be called once"); - - Handler = resultHandler; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Tapeti.Config; + +namespace Tapeti.Default +{ + internal class ControllerBindingContext : IControllerBindingContext + { + private BindingTargetMode? bindingTargetMode; + private readonly List middleware = new List(); + private readonly List parameters; + private readonly ControllerBindingResult result; + + /// + /// Determines how the binding target is configured. + /// + public BindingTargetMode BindingTargetMode => bindingTargetMode ?? BindingTargetMode.Default; + + + /// + /// Provides access to the registered middleware for this method. + /// + public IReadOnlyList Middleware => middleware; + + + /// + public Type MessageClass { get; set; } + + /// + public bool HasMessageClass => MessageClass != null; + + /// + public Type Controller { get; set; } + + /// + public MethodInfo Method { get; set; } + + /// + public IReadOnlyList Parameters => parameters; + + /// + public IBindingResult Result => result; + + + public ControllerBindingContext(IEnumerable parameters, ParameterInfo result) + { + this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList(); + + this.result = new ControllerBindingResult(result); + } + + + /// + public void SetMessageClass(Type messageClass) + { + if (HasMessageClass) + throw new InvalidOperationException("SetMessageClass can only be called once"); + + MessageClass = messageClass; + } + + + /// + public void SetBindingTargetMode(BindingTargetMode mode) + { + if (bindingTargetMode.HasValue) + throw new InvalidOperationException("SetBindingTargetMode can only be called once"); + + bindingTargetMode = mode; + } + + + /// + public void Use(IControllerMiddlewareBase handler) + { + middleware.Add(handler); + } + + + /// + /// Returns the configured bindings for the parameters. + /// + public IEnumerable GetParameterHandlers() + { + return parameters.Select(p => p.Binding); + } + + + /// + /// Returns the configured result handler. + /// + /// + public ResultHandler GetResultHandler() + { + return result.Handler; + } + } + + + /// + /// + /// Default implementation for IBindingParameter + /// + public class ControllerBindingParameter : IBindingParameter + { + /// + /// Provides access to the configured binding. + /// + public ValueFactory Binding { get; set; } + + + /// + public ParameterInfo Info { get; } + + /// + public bool HasBinding => Binding != null; + + + /// + public ControllerBindingParameter(ParameterInfo info) + { + Info = info; + } + + + /// + public void SetBinding(ValueFactory valueFactory) + { + if (Binding != null) + throw new InvalidOperationException("SetBinding can only be called once"); + + Binding = valueFactory; + } + } + + + /// + /// + /// Default implementation for IBindingResult + /// + public class ControllerBindingResult : IBindingResult + { + /// + /// Provides access to the configured handler. + /// + public ResultHandler Handler { get; set; } + + + /// + public ParameterInfo Info { get; } + + /// + public bool HasHandler => Handler != null; + + + /// + public ControllerBindingResult(ParameterInfo info) + { + Info = info; + } + + + /// + public void SetHandler(ResultHandler resultHandler) + { + if (Handler != null) + throw new InvalidOperationException("SetHandler can only be called once"); + + Handler = resultHandler; + } + } +} diff --git a/Tapeti/Default/DevNullLogger.cs b/Tapeti/Default/DevNullLogger.cs index bbaf911..9e712d7 100644 --- a/Tapeti/Default/DevNullLogger.cs +++ b/Tapeti/Default/DevNullLogger.cs @@ -33,10 +33,5 @@ namespace Tapeti.Default public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult) { } - - /// - public void QueueObsolete(string queueName, bool deleted, uint messageCount) - { - } } } diff --git a/Tapeti/ILogger.cs b/Tapeti/ILogger.cs index 0a16ba0..8fbabd8 100644 --- a/Tapeti/ILogger.cs +++ b/Tapeti/ILogger.cs @@ -110,6 +110,48 @@ namespace Tapeti /// /// Indicates the action taken by the exception handler void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult); + } + + + /// + /// Optional interface which can be implemented by an ILogger implementation to log all operations + /// related to declaring queues and bindings. + /// + public interface IBindingLogger : ILogger + { + /// + /// Called before a queue is declared for durable queues and dynamic queues with a prefix. Called after + /// a queue is declared for dynamic queues without a name with the queue name as determined by the RabbitMQ server. + /// Will always be called even if the queue already existed, as that information is not returned by the RabbitMQ server/client. + /// + /// The name of the queue that is declared + /// Indicates if the queue is durable or dynamic + /// Indicates whether the queue was declared as passive (to verify durable queues) + void QueueDeclare(string queueName, bool durable, bool passive); + + /// + /// Called before a binding is added to a queue. + /// + /// The name of the queue the binding is created for + /// Indicates if the queue is durable or dynamic + /// The exchange for the binding + /// The routing key for the binding + void QueueBind(string queueName, bool durable, string exchange, string routingKey); + + /// + /// Called before a binding is removed from a durable queue. + /// + /// The name of the queue the binding is removed from + /// The exchange of the binding + /// The routing key of the binding + void QueueUnbind(string queueName, string exchange, string routingKey); + + /// + /// Called before an exchange is declared. Will always be called once for each exchange involved in a dynamic queue, + /// durable queue with auto-declare bindings enabled or published messages, even if the exchange already existed. + /// + /// The name of the exchange that is declared + void ExchangeDeclare(string exchange); /// /// Called when a queue is determined to be obsolete. diff --git a/Tapeti/IPublisher.cs b/Tapeti/IPublisher.cs index 3a02ac3..417dc1a 100644 --- a/Tapeti/IPublisher.cs +++ b/Tapeti/IPublisher.cs @@ -1,50 +1,86 @@ -using System.Threading.Tasks; -using Tapeti.Config; - -// ReSharper disable once UnusedMember.Global - -namespace Tapeti -{ - /// - /// Allows publishing of messages. - /// - public interface IPublisher - { - /// - /// Publish the specified message. Transport details are determined by the Tapeti configuration. - /// - /// The message to send - Task Publish(object message); - } - - - /// - /// - /// Low-level publisher for Tapeti internal use. - /// - /// - /// Tapeti assumes every implementation of IPublisher can also be cast to an IInternalPublisher. - /// The distinction is made on purpose to trigger code-smells in non-Tapeti code when casting. - /// - public interface IInternalPublisher : IPublisher - { - /// - /// Publishes a message. The exchange and routing key are determined by the registered strategies. - /// - /// 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); - - - /// - /// Publishes a message directly to a queue. The exchange and routing key are not used. - /// - /// An instance of a message class - /// The name of the queue to send the message to - /// 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); - } -} +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Tapeti.Config; + +// ReSharper disable once UnusedMember.Global + +namespace Tapeti +{ + /// + /// Allows publishing of messages. + /// + public interface IPublisher + { + /// + /// Publish the specified message. Transport details are determined by the Tapeti configuration. + /// + /// The message to send + Task Publish(object message); + + + /// + /// Publish the specified request message and handle the response with the controller method as specified + /// by the responseMethodSelector expression. The response method or controller must have a valid queue attribute. + /// + /// + /// The response method is called on a new instance of the controller, as is the case with a regular message. + /// To preserve state, use the Tapeti.Flow extension instead. + /// + /// 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; + + + /// + /// Publish the specified request message and handle the response with the controller method as specified + /// by the responseMethodSelector expression. The response method or controller must have a valid queue attribute. + /// + /// + /// The response method is called on a new instance of the controller, as is the case with a regular message. + /// To preserve state, use the Tapeti.Flow extension instead. + /// + /// 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; + + + /// + /// Sends a message directly to the specified queue. Not recommended for general use. + /// + /// The name of the queue to publish the message to + /// The message to send + Task SendToQueue(string queueName, object message); + } + + + /// + /// + /// Low-level publisher for Tapeti internal use. + /// + /// + /// Tapeti assumes every implementation of IPublisher can also be cast to an IInternalPublisher. + /// The distinction is made on purpose to trigger code-smells in non-Tapeti code when casting. + /// + public interface IInternalPublisher : IPublisher + { + /// + /// Publishes a message. The exchange and routing key are determined by the registered strategies. + /// + /// 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); + + + /// + /// Publishes a message directly to a queue. The exchange and routing key are not used. + /// + /// An instance of a message class + /// The name of the queue to send the message to + /// 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); + } +} diff --git a/Tapeti/TapetiConfig.cs b/Tapeti/TapetiConfig.cs index 7a542a1..6da6fae 100644 --- a/Tapeti/TapetiConfig.cs +++ b/Tapeti/TapetiConfig.cs @@ -1,311 +1,317 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Tapeti.Config; -using Tapeti.Default; -using Tapeti.Helpers; - -// ReSharper disable UnusedMember.Global - -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 - { - private Config config; - private readonly List bindingMiddleware = new List(); - - - /// - public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver; - - - /// - /// Instantiates a new Tapeti config builder. - /// - /// A wrapper implementation for an IoC container to allow dependency injection - public TapetiConfig(IDependencyResolver dependencyResolver) - { - config = new Config(dependencyResolver); - - Use(new DependencyResolverBinding()); - Use(new PublishResultBinding()); - - // Registered last so it runs first and the MessageClass is known to other middleware - Use(new MessageBinding()); - } - - - /// - public ITapetiConfig Build() - { - if (config == null) - throw new InvalidOperationException("TapetiConfig.Build must only be called once"); - - RegisterDefaults(); - (config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton(config); - - - var outputConfig = config; - config = null; - - outputConfig.Lock(); - return outputConfig; - } - - - /// - public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler) - { - bindingMiddleware.Add(handler); - return this; - } - - - /// - public ITapetiConfigBuilder Use(IMessageMiddleware handler) - { - GetConfig().Use(handler); - return this; - } - - - /// - public ITapetiConfigBuilder Use(IPublishMiddleware handler) - { - GetConfig().Use(handler); - return this; - } - - - /// - public ITapetiConfigBuilder Use(ITapetiExtension extension) - { - if (DependencyResolver is IDependencyContainer container) - extension.RegisterDefaults(container); - - var configInstance = GetConfig(); - - var middlewareBundle = extension.GetMiddleware(DependencyResolver); - if (middlewareBundle != null) - { - foreach (var middleware in middlewareBundle) - { - switch (middleware) - { - case IControllerBindingMiddleware bindingExtension: - Use(bindingExtension); - break; - - case IMessageMiddleware messageExtension: - configInstance.Use(messageExtension); - break; - - case IPublishMiddleware publishExtension: - configInstance.Use(publishExtension); - break; - - default: - throw new ArgumentException( - $"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}"); - } - } - } - - var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver); - if (bindingBundle == null) - return this; - - foreach (var binding in bindingBundle) - config.RegisterBinding(binding); - - return this; - } - - - /// - public void RegisterBinding(IBinding binding) - { - GetConfig().RegisterBinding(binding); - } - - - /// - public ITapetiConfigBuilder DisablePublisherConfirms() - { - GetConfig().SetPublisherConfirms(false); - return this; - } - - - /// - public ITapetiConfigBuilder SetPublisherConfirms(bool enabled) - { - GetConfig().SetPublisherConfirms(enabled); - return this; - } - - - /// - public ITapetiConfigBuilder EnableDeclareDurableQueues() - { - GetConfig().SetDeclareDurableQueues(true); - return this; - } - - - /// - public ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled) - { - GetConfig().SetDeclareDurableQueues(enabled); - return this; - } - - - /// - /// Registers the default implementation of various Tapeti interfaces into the IoC container. - /// - protected void RegisterDefaults() - { - if (!(DependencyResolver is IDependencyContainer container)) - return; - - if (ConsoleHelper.IsAvailable()) - container.RegisterDefault(); - else - container.RegisterDefault(); - - container.RegisterDefault(); - container.RegisterDefault(); - container.RegisterDefault(); - container.RegisterDefault(); - } - - - /// - public void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler) - { - MiddlewareHelper.Go(bindingMiddleware, - (handler, next) => handler.Handle(context, next), - lastHandler); - } - - - private Config GetConfig() - { - if (config == null) - throw new InvalidOperationException("TapetiConfig can not be updated after Build"); - - return config; - } - - - /// - internal class Config : ITapetiConfig - { - private readonly ConfigFeatures features = new ConfigFeatures(); - private readonly ConfigMiddleware middleware = new ConfigMiddleware(); - private readonly ConfigBindings bindings = new ConfigBindings(); - - public IDependencyResolver DependencyResolver { get; } - public ITapetiConfigFeatues Features => features; - public ITapetiConfigMiddleware Middleware => middleware; - public ITapetiConfigBindings Bindings => bindings; - - - public Config(IDependencyResolver dependencyResolver) - { - DependencyResolver = dependencyResolver; - } - - - public void Lock() - { - bindings.Lock(); - } - - - public void Use(IMessageMiddleware handler) - { - middleware.Use(handler); - } - - public void Use(IPublishMiddleware handler) - { - middleware.Use(handler); - } - - - public void RegisterBinding(IBinding binding) - { - bindings.Add(binding); - } - - - public void SetPublisherConfirms(bool enabled) - { - features.PublisherConfirms = enabled; - } - - public void SetDeclareDurableQueues(bool enabled) - { - features.DeclareDurableQueues = enabled; - } - } - - - internal class ConfigFeatures : ITapetiConfigFeatues - { - public bool PublisherConfirms { get; internal set; } = true; - public bool DeclareDurableQueues { get; internal set; } = true; - } - - - internal class ConfigMiddleware : ITapetiConfigMiddleware - { - private readonly List messageMiddleware = new List(); - private readonly List publishMiddleware = new List(); - - - public IReadOnlyList Message => messageMiddleware; - public IReadOnlyList Publish => publishMiddleware; - - - public void Use(IMessageMiddleware handler) - { - messageMiddleware.Add(handler); - } - - public void Use(IPublishMiddleware handler) - { - publishMiddleware.Add(handler); - } - } - - - internal class ConfigBindings : List, ITapetiConfigBindings - { - private Dictionary methodLookup; - - - public IControllerMethodBinding ForMethod(Delegate method) - { - return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null; - } - - - public void Lock() - { - methodLookup = this - .Where(binding => binding is IControllerMethodBinding) - .Cast() - .ToDictionary(binding => binding.Method, binding => binding); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Tapeti.Config; +using Tapeti.Default; +using Tapeti.Helpers; + +// ReSharper disable UnusedMember.Global + +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 + { + private Config config; + private readonly List bindingMiddleware = new List(); + + + /// + public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver; + + + /// + /// Instantiates a new Tapeti config builder. + /// + /// A wrapper implementation for an IoC container to allow dependency injection + public TapetiConfig(IDependencyResolver dependencyResolver) + { + config = new Config(dependencyResolver); + + Use(new DependencyResolverBinding()); + Use(new PublishResultBinding()); + + // Registered last so it runs first and the MessageClass is known to other middleware + Use(new MessageBinding()); + } + + + /// + public ITapetiConfig Build() + { + if (config == null) + throw new InvalidOperationException("TapetiConfig.Build must only be called once"); + + RegisterDefaults(); + (config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton(config); + + + var outputConfig = config; + config = null; + + outputConfig.Lock(); + return outputConfig; + } + + + /// + public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler) + { + bindingMiddleware.Add(handler); + return this; + } + + + /// + public ITapetiConfigBuilder Use(IMessageMiddleware handler) + { + GetConfig().Use(handler); + return this; + } + + + /// + public ITapetiConfigBuilder Use(IPublishMiddleware handler) + { + GetConfig().Use(handler); + return this; + } + + + /// + public ITapetiConfigBuilder Use(ITapetiExtension extension) + { + if (DependencyResolver is IDependencyContainer container) + extension.RegisterDefaults(container); + + var configInstance = GetConfig(); + + var middlewareBundle = extension.GetMiddleware(DependencyResolver); + if (middlewareBundle != null) + { + foreach (var middleware in middlewareBundle) + { + switch (middleware) + { + case IControllerBindingMiddleware bindingExtension: + Use(bindingExtension); + break; + + case IMessageMiddleware messageExtension: + configInstance.Use(messageExtension); + break; + + case IPublishMiddleware publishExtension: + configInstance.Use(publishExtension); + break; + + default: + throw new ArgumentException( + $"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}"); + } + } + } + + var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver); + if (bindingBundle == null) + return this; + + foreach (var binding in bindingBundle) + config.RegisterBinding(binding); + + return this; + } + + + /// + public void RegisterBinding(IBinding binding) + { + GetConfig().RegisterBinding(binding); + } + + + /// + public ITapetiConfigBuilder DisablePublisherConfirms() + { + GetConfig().SetPublisherConfirms(false); + return this; + } + + + /// + public ITapetiConfigBuilder SetPublisherConfirms(bool enabled) + { + GetConfig().SetPublisherConfirms(enabled); + return this; + } + + + /// + public ITapetiConfigBuilder EnableDeclareDurableQueues() + { + GetConfig().SetDeclareDurableQueues(true); + return this; + } + + + /// + public ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled) + { + GetConfig().SetDeclareDurableQueues(enabled); + return this; + } + + + /// + /// Registers the default implementation of various Tapeti interfaces into the IoC container. + /// + protected void RegisterDefaults() + { + if (!(DependencyResolver is IDependencyContainer container)) + return; + + if (ConsoleHelper.IsAvailable()) + container.RegisterDefault(); + else + container.RegisterDefault(); + + container.RegisterDefault(); + container.RegisterDefault(); + container.RegisterDefault(); + container.RegisterDefault(); + } + + + /// + public void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler) + { + MiddlewareHelper.Go(bindingMiddleware, + (handler, next) => handler.Handle(context, next), + lastHandler); + } + + + private Config GetConfig() + { + if (config == null) + throw new InvalidOperationException("TapetiConfig can not be updated after Build"); + + return config; + } + + + /// + internal class Config : ITapetiConfig + { + private readonly ConfigFeatures features = new ConfigFeatures(); + private readonly ConfigMiddleware middleware = new ConfigMiddleware(); + private readonly ConfigBindings bindings = new ConfigBindings(); + + public IDependencyResolver DependencyResolver { get; } + public ITapetiConfigFeatues Features => features; + public ITapetiConfigMiddleware Middleware => middleware; + public ITapetiConfigBindings Bindings => bindings; + + + public Config(IDependencyResolver dependencyResolver) + { + DependencyResolver = dependencyResolver; + } + + + public void Lock() + { + bindings.Lock(); + } + + + public void Use(IMessageMiddleware handler) + { + middleware.Use(handler); + } + + public void Use(IPublishMiddleware handler) + { + middleware.Use(handler); + } + + + public void RegisterBinding(IBinding binding) + { + bindings.Add(binding); + } + + + public void SetPublisherConfirms(bool enabled) + { + features.PublisherConfirms = enabled; + } + + public void SetDeclareDurableQueues(bool enabled) + { + features.DeclareDurableQueues = enabled; + } + } + + + internal class ConfigFeatures : ITapetiConfigFeatues + { + public bool PublisherConfirms { get; internal set; } = true; + public bool DeclareDurableQueues { get; internal set; } = true; + } + + + internal class ConfigMiddleware : ITapetiConfigMiddleware + { + private readonly List messageMiddleware = new List(); + private readonly List publishMiddleware = new List(); + + + public IReadOnlyList Message => messageMiddleware; + public IReadOnlyList Publish => publishMiddleware; + + + public void Use(IMessageMiddleware handler) + { + messageMiddleware.Add(handler); + } + + public void Use(IPublishMiddleware handler) + { + publishMiddleware.Add(handler); + } + } + + + internal class ConfigBindings : List, ITapetiConfigBindings + { + private Dictionary methodLookup; + + + public IControllerMethodBinding ForMethod(Delegate method) + { + return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null; + } + + + public IControllerMethodBinding ForMethod(MethodInfo method) + { + return methodLookup.TryGetValue(method, out var binding) ? binding : null; + } + + + public void Lock() + { + methodLookup = this + .Where(binding => binding is IControllerMethodBinding) + .Cast() + .ToDictionary(binding => binding.Method, binding => binding); + } + } + } +} diff --git a/Tapeti/TapetiConfigControllers.cs b/Tapeti/TapetiConfigControllers.cs index dcd8127..aaaddf6 100644 --- a/Tapeti/TapetiConfigControllers.cs +++ b/Tapeti/TapetiConfigControllers.cs @@ -1,147 +1,150 @@ -using System; -using System.Linq; -using System.Reflection; -using Tapeti.Annotations; -using Tapeti.Config; -using Tapeti.Default; - -// ReSharper disable UnusedMember.Global - -namespace Tapeti -{ - /// - /// - /// Thrown when an issue is detected in a controller configuration. - /// - public class TopologyConfigurationException : Exception - { - /// - public TopologyConfigurationException(string message) : base(message) { } - } - - - /// - /// Extension methods for registering message controllers. - /// - public static class TapetiConfigControllers - { - /// - /// Registers all public methods in the specified controller class as message handlers. - /// - /// - /// The controller class to register. The class and/or methods must be annotated with either the DurableQueue or DynamicQueue attribute. - public static ITapetiConfigBuilder RegisterController(this ITapetiConfigBuilder builder, Type controller) - { - var builderAccess = (ITapetiConfigBuilderAccess)builder; - - if (!controller.IsClass) - throw new ArgumentException($"Controller {controller.Name} must be a class"); - - var controllerQueueInfo = GetQueueInfo(controller); - (builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller); - - var controllerIsObsolete = controller.GetCustomAttribute() != null; - - - foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance) - .Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false) - .Select(m => (MethodInfo)m)) - { - var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo; - if (methodQueueInfo == null || !methodQueueInfo.IsValid) - throw new TopologyConfigurationException( - $"Method {method.Name} or controller {controller.Name} requires a queue attribute"); - - - var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute() != null; - - - var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter) - { - Controller = controller, - Method = method - }; - - - var allowBinding = false; - builderAccess.ApplyBindingMiddleware(context, () => { allowBinding = true; }); - - if (!allowBinding) - continue; - - - if (context.MessageClass == null) - throw new TopologyConfigurationException($"Method {method.Name} in controller {controller.Name} does not resolve to a message class"); - - - var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList(); - if (invalidBindings.Count > 0) - { - var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name)); - throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}"); - } - - - builder.RegisterBinding(new ControllerMethodBinding(builderAccess.DependencyResolver, new ControllerMethodBinding.BindingInfo - { - ControllerType = controller, - Method = method, - QueueInfo = methodQueueInfo, - MessageClass = context.MessageClass, - BindingTargetMode = context.BindingTargetMode, - IsObsolete = methodIsObsolete, - ParameterFactories = context.GetParameterHandlers(), - ResultHandler = context.GetResultHandler(), - - FilterMiddleware = context.Middleware.Where(m => m is IControllerFilterMiddleware).Cast().ToList(), - MessageMiddleware = context.Middleware.Where(m => m is IControllerMessageMiddleware).Cast().ToList(), - CleanupMiddleware = context.Middleware.Where(m => m is IControllerCleanupMiddleware).Cast().ToList() - })); - - } - - return builder; - } - - - /// - /// Registers all controllers in the specified assembly which are marked with the MessageController attribute. - /// - /// - /// The assembly to scan for controllers. - public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder, Assembly assembly) - { - foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute)))) - RegisterController(builder, type); - - return builder; - } - - - /// - /// Registers all controllers in the entry assembly which are marked with the MessageController attribute. - /// - /// - public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder) - { - return RegisterAllControllers(builder, Assembly.GetEntryAssembly()); - } - - - private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member) - { - var dynamicQueueAttribute = member.GetCustomAttribute(); - var durableQueueAttribute = 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 }; - - return durableQueueAttribute != null - ? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name } - : null; - } - } -} +using System; +using System.Linq; +using System.Reflection; +using Tapeti.Annotations; +using Tapeti.Config; +using Tapeti.Default; + +// ReSharper disable UnusedMember.Global + +namespace Tapeti +{ + /// + /// + /// Thrown when an issue is detected in a controller configuration. + /// + public class TopologyConfigurationException : Exception + { + /// + public TopologyConfigurationException(string message) : base(message) { } + } + + + /// + /// Extension methods for registering message controllers. + /// + public static class TapetiConfigControllers + { + /// + /// Registers all public methods in the specified controller class as message handlers. + /// + /// + /// The controller class to register. The class and/or methods must be annotated with either the DurableQueue or DynamicQueue attribute. + public static ITapetiConfigBuilder RegisterController(this ITapetiConfigBuilder builder, Type controller) + { + var builderAccess = (ITapetiConfigBuilderAccess)builder; + + if (!controller.IsClass) + throw new ArgumentException($"Controller {controller.Name} must be a class"); + + var controllerQueueInfo = GetQueueInfo(controller); + (builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller); + + var controllerIsObsolete = controller.GetCustomAttribute() != null; + + + foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false) + .Select(m => (MethodInfo)m)) + { + var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo; + if (methodQueueInfo == null || !methodQueueInfo.IsValid) + throw new TopologyConfigurationException( + $"Method {method.Name} or controller {controller.Name} requires a queue attribute"); + + + var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute() != null; + + + var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter) + { + Controller = controller, + Method = method + }; + + + if (method.GetCustomAttribute() != null) + context.SetBindingTargetMode(BindingTargetMode.Direct); + + + var allowBinding = false; + builderAccess.ApplyBindingMiddleware(context, () => { allowBinding = true; }); + + if (!allowBinding) + continue; + + + if (context.MessageClass == null) + throw new TopologyConfigurationException($"Method {method.Name} in controller {controller.Name} does not resolve to a message class"); + + + var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList(); + if (invalidBindings.Count > 0) + { + var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name)); + throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}"); + } + + + builder.RegisterBinding(new ControllerMethodBinding(builderAccess.DependencyResolver, new ControllerMethodBinding.BindingInfo + { + ControllerType = controller, + Method = method, + QueueInfo = methodQueueInfo, + MessageClass = context.MessageClass, + BindingTargetMode = context.BindingTargetMode, + IsObsolete = methodIsObsolete, + ParameterFactories = context.GetParameterHandlers(), + ResultHandler = context.GetResultHandler(), + + FilterMiddleware = context.Middleware.Where(m => m is IControllerFilterMiddleware).Cast().ToList(), + MessageMiddleware = context.Middleware.Where(m => m is IControllerMessageMiddleware).Cast().ToList(), + CleanupMiddleware = context.Middleware.Where(m => m is IControllerCleanupMiddleware).Cast().ToList() + })); + } + + return builder; + } + + + /// + /// Registers all controllers in the specified assembly which are marked with the MessageController attribute. + /// + /// + /// The assembly to scan for controllers. + public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder, Assembly assembly) + { + foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute)))) + RegisterController(builder, type); + + return builder; + } + + + /// + /// Registers all controllers in the entry assembly which are marked with the MessageController attribute. + /// + /// + public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder) + { + return RegisterAllControllers(builder, Assembly.GetEntryAssembly()); + } + + + private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member) + { + var dynamicQueueAttribute = member.GetCustomAttribute(); + var durableQueueAttribute = 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 }; + + return durableQueueAttribute != null + ? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name } + : null; + } + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ccea1cc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +The documentation can be built locally using Sphinx. Install Python 3 (choco install python on Windows), +then install sphinx and the ReadTheDocs theme: + +```pip install sphinx sphinx_rtd_theme``` + +To build the HTML output, run: + +```.\make.bat html``` + + + +To use the auto reloading server (rundev.bat), install the sphinx-autobuild package: + +```pip install sphinx-autobuild``` \ No newline at end of file diff --git a/docs/flow.rst b/docs/flow.rst index d7705bd..25cd90f 100644 --- a/docs/flow.rst +++ b/docs/flow.rst @@ -1,291 +1,291 @@ -Flow extension -============== - -*Flow* in the context of Tapeti is inspired by what is referred to as a Saga or Conversation in messaging. It enables a controller to communicate with other services, temporarily yielding it's execution while waiting for a response. When the response arrives the controller will resume, retaining the original state of it's public fields. - -This process is fully asynchronous, the service initiating the flow can be restarted and the flow will continue when the service is back up (assuming the queues are durable and a persistent flow state store is used). - - -Request - response pattern --------------------------- -Tapeti implements the request - response pattern by allowing a message handler method to simply return the response message. Tapeti Flow extends on this concept by allowing the sender of the request to maintain it's state for when the response arrives. - -See :doc:`indepth` on defining request - response messages. - -Enabling Tapeti Flow --------------------- -To enable the use of Tapeti Flow, install the Tapeti.Flow NuGet package and call ``WithFlow()`` when setting up your TapetiConfig: - -:: - - var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) - .WithFlow() - .RegisterAllControllers() - .Build(); - -Starting a flow ---------------- -To start a new flow you need to obtain an IFlowStarter from your IoC container. It has one method in various overloads: ``Start``. - -Flow requires all methods participating in the flow, including the starting method, to be in the same controller. This allows the state to be stored and restored when the flow continues. The ``IFlowStarter.Start`` call does not need to be in the controller class. - -The controller type is passed as a generic parameter. The first parameter to the Start method is a method selector. This defines which method in the controller is called as soon as the flow is initialised. - -:: - - await flowStart.Start(c => c.StartFlow); - -The start method can have any name, but must be annotated with the ``[Start]`` attribute. This ensures it is not recognized as a message handler. The start method and any further continuation methods must return either Task (for asynchronous methods) or simply IYieldPoint (for synchronous methods). - -:: - - [MessageController] - [DynamicQueue] - public class QueryBunniesController - { - public DateTime RequestStart { get; set; } - - [Start] - IYieldPoint StartFlow() - { - RequestStart = DateTime.UtcNow(); - } - } - - - -Often you'll want to pass some initial information to the flow. The Start method allows one parameter. If you need more information, bundle it in a class or struct. - -:: - - await flowStart.Start(c => c.StartFlow, "pink"); - - [MessageController] - [DynamicQueue] - public class QueryBunniesController - { - public DateTime RequestStart { get; set; } - - [Start] - IYieldPoint StartFlow(string colorFilter) - { - RequestStart = DateTime.UtcNow(); - } - } - - -.. note:: Every time a flow is started or continued a new instance of the controller is created. All public fields in the controller are considered part of the state and will be restored when a response arrives, private and protected fields are not. Public fields must be serializable to JSON (using JSON.NET) to retain their value when a flow continues. Try to minimize the amount of state as it is cached in memory until the flow ends. - -Continuing a flow ------------------ -When starting a flow you're most likely want to start with a request message. Similarly, when continuing a flow you have the option to follow it up with another request and prolong the flow. This behaviour is controlled by the IYieldPoint that must be returned from the start and continuation handlers. To get an IYieldPoint you need to inject the IFlowProvider into your controller. - -IFlowProvider has a method ``YieldWithRequest`` which sends the provided request message and restores the controller when the response arrives, calling the response handler method you pass along to it. - -The response handler must be marked with the ``[Continuation]`` attribute. This ensures it is never called for broadcast messages, only when the response for our specific request arrives. It must also return an IYieldPoint or Task itself. - -If the response handler is not asynchronous, use ``YieldWithRequestSync`` instead, as used in the example below: - -:: - - [MessageController] - [DynamicQueue] - public class QueryBunniesController - { - private IFlowProvider flowProvider; - - public DateTime RequestStart { get; set; } - - - public QueryBunniesController(IFlowProvider flowProvider) - { - this.flowProvider = flowProvider; - } - - [Start] - IYieldPoint StartFlow(string colorFilter) - { - RequestStart = DateTime.UtcNow(); - - var request = new BunnyCountRequestMessage - { - ColorFilter = colorFilter - }; - - return flowProvider.YieldWithRequestSync - (request, HandleBunnyCountResponse); - } - - - [Continuation] - public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) - { - // Handle the response. The original RequestStart is available here as well. - } - } - -You can once again return a ``YieldWithRequest``, or end it. - -Ending a flow -------------- -To end the flow and dispose of any stored state, return an end yieldpoint: - -:: - - [Continuation] - public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) - { - // Handle the response. - - return flowProvider.End(); - } - - -Flows started by a (request) message ------------------------------------- -Instead of manually starting a flow, you can also start one in response to an incoming message. You do not need access to the IFlowStarter in that case, simply return an IYieldPoint from a regular message handler: - -:: - - [MessageController] - [DurableQueue("hutch")] - public class HutchController - { - private IBunnyRepository repository; - private IFlowProvider flowProvider; - - public string ColorFilter { get; set; } - - - public HutchController(IBunnyRepository repository, IFlowProvider flowProvider) - { - this.repository = repository; - this.flowProvider = flowProvider; - } - - public IYieldPoint HandleCountRequest(BunnyCountRequestMessage message) - { - ColorFilter = message.ColorFilter; - - return flowProvider.YieldWithRequestSync - ( - new CheckAccessRequestMessage - { - Username = "hutch" - }, - HandleCheckAccessResponseMessage - ); - } - - - [Continuation] - public IYieldPoint HandleCheckAccessResponseMessage(CheckAccessResponseMessage message) - { - // We must provide a response to our original BunnyCountRequestMessage - return flowProvider.EndWithResponse(new BunnyCountResponseMessage - { - Count = message.HasAccess ? await repository.Count(ColorFilter) : 0 - }); - } - -.. note:: If the message that started the flow was a request message, you must end the flow with EndWithResponse or you will get an exception. Likewise, if the message was not a request message, you must end the flow with End. - - -Parallel requests ------------------ -When you want to send out more than one request, you could chain them in the response handler for each message. An easier way is to use ``YieldWithParallelRequest``. It returns a parallel request builder to which you can add one or more requests to be sent out, each with it's own response handler. In the end, the Yield method of the builder can be used to create a YieldPoint. It also specifies the converge method which is called when all responses have been handled. - -An example: - -:: - - public IYieldPoint HandleBirthdayMessage(RabbitBirthdayMessage message) - { - var sendCardRequest = new SendCardRequestMessage - { - RabbitID = message.RabbitID, - Age = message.Age, - Style = CardStyles.Funny - }; - - var doctorAppointmentMessage = new DoctorAppointmentRequestMessage - { - RabbitID = message.RabbitID, - Reason = "Yearly checkup" - }; - - return flowProvider.YieldWithParallelRequest() - .AddRequestSync( - sendCardRequest, HandleCardResponse) - - .AddRequestSync( - doctorAppointmentMessage, HandleDoctorAppointmentResponse) - - .YieldSync(ContinueAfterResponses); - } - - [Continuation] - public void HandleCardResponse(SendCardResponseMessage message) - { - // Handle card response. For example, store the result in a public field - } - - [Continuation] - public void HandleDoctorAppointmentResponse(DoctorAppointmentResponseMessage message) - { - // Handle appointment response. Note that the order of the responses is not guaranteed, - // but the handlers will never run at the same time, so it is safe to access - // and manipulate the public fields of the controller. - } - - private IYieldPoint ContinueAfterResponses() - { - // Perform further operations on the results stored in the public fields - - // This flow did not start with a request message, so end it normally - return flowProvider.End(); - } - - -A few things to note: - -#) The response handlers do not return an IYieldPoint themselves, but void (for AddRequestSync) or Task (for AddRequest). Therefore they can not influence the flow. Instead the converge method as passed to Yield or YieldSync determines how the flow continues. It is called immediately after the last response handler. -#) The converge method must be private, as it is not a valid message handler in itself. -#) You must add at least one request. - -Note that you do not have to perform all the operations in one go. You can store the result of ``YieldWithParallelRequest`` and conditionally call ``AddRequest`` or ``AddRequestSync`` as many times as required. - - -Persistent state ----------------- -By default flow state is only preserved while the service is running. To persist the flow state across restarts and reboots, provide an implementation of IFlowRepository to ``WithFlow()``. - -:: - - var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) - .WithFlow(new MyFlowRepository()) - .RegisterAllControllers() - .Build(); - - -Tapeti.Flow includes an implementation for SQL server you can use as well. First, make sure your database contains a table to store flow state: - -:: - - create table Flow - ( - FlowID uniqueidentifier not null, - CreationTime datetime2(3) not null, - StateJson nvarchar(max) null, - constraint PK_Flow primary key clustered(FlowID) - ); - -Then install the Tapeti.Flow.SQL NuGet package and register the SqlConnectionFlowRepository by passing it to WithFlow, or by using the ``WithFlowSqlRepository`` extension method before calling ``WithFlow``: - -:: - - var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) - .WithFlowSqlRepository("Server=localhost;Database=TapetiTest;Integrated Security=true") - .WithFlow() - .RegisterAllControllers() - .Build(); +Flow extension +============== + +*Flow* in the context of Tapeti is inspired by what is referred to as a Saga or Conversation in messaging. It enables a controller to communicate with other services, temporarily yielding it's execution while waiting for a response. When the response arrives the controller will resume, retaining the original state of it's public fields. + +This process is fully asynchronous, the service initiating the flow can be restarted and the flow will continue when the service is back up (assuming the queues are durable and a persistent flow state store is used). + + +Request - response pattern +-------------------------- +Tapeti implements the request - response pattern by allowing a message handler method to simply return the response message. Tapeti Flow extends on this concept by allowing the sender of the request to maintain it's state for when the response arrives. + +See :doc:`indepth` on defining request - response messages. + +Enabling Tapeti Flow +-------------------- +To enable the use of Tapeti Flow, install the Tapeti.Flow NuGet package and call ``WithFlow()`` when setting up your TapetiConfig: + +:: + + var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) + .WithFlow() + .RegisterAllControllers() + .Build(); + +Starting a flow +--------------- +To start a new flow you need to obtain an IFlowStarter from your IoC container. It has one method in various overloads: ``Start``. + +Flow requires all methods participating in the flow, including the starting method, to be in the same controller. This allows the state to be stored and restored when the flow continues. The ``IFlowStarter.Start`` call does not need to be in the controller class. + +The controller type is passed as a generic parameter. The first parameter to the Start method is a method selector. This defines which method in the controller is called as soon as the flow is initialised. + +:: + + await flowStart.Start(c => c.StartFlow); + +The start method can have any name, but must be annotated with the ``[Start]`` attribute. This ensures it is not recognized as a message handler. The start method and any further continuation methods must return either Task (for asynchronous methods) or simply IYieldPoint (for synchronous methods). + +:: + + [MessageController] + [DynamicQueue] + public class QueryBunniesController + { + public DateTime RequestStart { get; set; } + + [Start] + public IYieldPoint StartFlow() + { + RequestStart = DateTime.UtcNow(); + } + } + + + +Often you'll want to pass some initial information to the flow. The Start method allows one parameter. If you need more information, bundle it in a class or struct. + +:: + + await flowStart.Start(c => c.StartFlow, "pink"); + + [MessageController] + [DynamicQueue] + public class QueryBunniesController + { + public DateTime RequestStart { get; set; } + + [Start] + public IYieldPoint StartFlow(string colorFilter) + { + RequestStart = DateTime.UtcNow(); + } + } + + +.. note:: Every time a flow is started or continued a new instance of the controller is created. All public fields in the controller are considered part of the state and will be restored when a response arrives, private and protected fields are not. Public fields must be serializable to JSON (using JSON.NET) to retain their value when a flow continues. Try to minimize the amount of state as it is cached in memory until the flow ends. + +Continuing a flow +----------------- +When starting a flow you're most likely want to start with a request message. Similarly, when continuing a flow you have the option to follow it up with another request and prolong the flow. This behaviour is controlled by the IYieldPoint that must be returned from the start and continuation handlers. To get an IYieldPoint you need to inject the IFlowProvider into your controller. + +IFlowProvider has a method ``YieldWithRequest`` which sends the provided request message and restores the controller when the response arrives, calling the response handler method you pass along to it. + +The response handler must be marked with the ``[Continuation]`` attribute. This ensures it is never called for broadcast messages, only when the response for our specific request arrives. It must also return an IYieldPoint or Task itself. + +If the response handler is not asynchronous, use ``YieldWithRequestSync`` instead, as used in the example below: + +:: + + [MessageController] + [DynamicQueue] + public class QueryBunniesController + { + private IFlowProvider flowProvider; + + public DateTime RequestStart { get; set; } + + + public QueryBunniesController(IFlowProvider flowProvider) + { + this.flowProvider = flowProvider; + } + + [Start] + public IYieldPoint StartFlow(string colorFilter) + { + RequestStart = DateTime.UtcNow(); + + var request = new BunnyCountRequestMessage + { + ColorFilter = colorFilter + }; + + return flowProvider.YieldWithRequestSync + (request, HandleBunnyCountResponse); + } + + + [Continuation] + public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) + { + // Handle the response. The original RequestStart is available here as well. + } + } + +You can once again return a ``YieldWithRequest``, or end it. + +Ending a flow +------------- +To end the flow and dispose of any stored state, return an end yieldpoint: + +:: + + [Continuation] + public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) + { + // Handle the response. + + return flowProvider.End(); + } + + +Flows started by a (request) message +------------------------------------ +Instead of manually starting a flow, you can also start one in response to an incoming message. You do not need access to the IFlowStarter in that case, simply return an IYieldPoint from a regular message handler: + +:: + + [MessageController] + [DurableQueue("hutch")] + public class HutchController + { + private IBunnyRepository repository; + private IFlowProvider flowProvider; + + public string ColorFilter { get; set; } + + + public HutchController(IBunnyRepository repository, IFlowProvider flowProvider) + { + this.repository = repository; + this.flowProvider = flowProvider; + } + + public IYieldPoint HandleCountRequest(BunnyCountRequestMessage message) + { + ColorFilter = message.ColorFilter; + + return flowProvider.YieldWithRequestSync + ( + new CheckAccessRequestMessage + { + Username = "hutch" + }, + HandleCheckAccessResponseMessage + ); + } + + + [Continuation] + public IYieldPoint HandleCheckAccessResponseMessage(CheckAccessResponseMessage message) + { + // We must provide a response to our original BunnyCountRequestMessage + return flowProvider.EndWithResponse(new BunnyCountResponseMessage + { + Count = message.HasAccess ? await repository.Count(ColorFilter) : 0 + }); + } + +.. note:: If the message that started the flow was a request message, you must end the flow with EndWithResponse or you will get an exception. Likewise, if the message was not a request message, you must end the flow with End. + + +Parallel requests +----------------- +When you want to send out more than one request, you could chain them in the response handler for each message. An easier way is to use ``YieldWithParallelRequest``. It returns a parallel request builder to which you can add one or more requests to be sent out, each with it's own response handler. In the end, the Yield method of the builder can be used to create a YieldPoint. It also specifies the converge method which is called when all responses have been handled. + +An example: + +:: + + public IYieldPoint HandleBirthdayMessage(RabbitBirthdayMessage message) + { + var sendCardRequest = new SendCardRequestMessage + { + RabbitID = message.RabbitID, + Age = message.Age, + Style = CardStyles.Funny + }; + + var doctorAppointmentMessage = new DoctorAppointmentRequestMessage + { + RabbitID = message.RabbitID, + Reason = "Yearly checkup" + }; + + return flowProvider.YieldWithParallelRequest() + .AddRequestSync( + sendCardRequest, HandleCardResponse) + + .AddRequestSync( + doctorAppointmentMessage, HandleDoctorAppointmentResponse) + + .YieldSync(ContinueAfterResponses); + } + + [Continuation] + public void HandleCardResponse(SendCardResponseMessage message) + { + // Handle card response. For example, store the result in a public field + } + + [Continuation] + public void HandleDoctorAppointmentResponse(DoctorAppointmentResponseMessage message) + { + // Handle appointment response. Note that the order of the responses is not guaranteed, + // but the handlers will never run at the same time, so it is safe to access + // and manipulate the public fields of the controller. + } + + private IYieldPoint ContinueAfterResponses() + { + // Perform further operations on the results stored in the public fields + + // This flow did not start with a request message, so end it normally + return flowProvider.End(); + } + + +A few things to note: + +#) The response handlers do not return an IYieldPoint themselves, but void (for AddRequestSync) or Task (for AddRequest). Therefore they can not influence the flow. Instead the converge method as passed to Yield or YieldSync determines how the flow continues. It is called immediately after the last response handler. +#) The converge method must be private, as it is not a valid message handler in itself. +#) You must add at least one request. + +Note that you do not have to perform all the operations in one go. You can store the result of ``YieldWithParallelRequest`` and conditionally call ``AddRequest`` or ``AddRequestSync`` as many times as required. + + +Persistent state +---------------- +By default flow state is only preserved while the service is running. To persist the flow state across restarts and reboots, provide an implementation of IFlowRepository to ``WithFlow()``. + +:: + + var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) + .WithFlow(new MyFlowRepository()) + .RegisterAllControllers() + .Build(); + + +Tapeti.Flow includes an implementation for SQL server you can use as well. First, make sure your database contains a table to store flow state: + +:: + + create table Flow + ( + FlowID uniqueidentifier not null, + CreationTime datetime2(3) not null, + StateJson nvarchar(max) null, + constraint PK_Flow primary key clustered(FlowID) + ); + +Then install the Tapeti.Flow.SQL NuGet package and register the SqlConnectionFlowRepository by passing it to WithFlow, or by using the ``WithFlowSqlRepository`` extension method before calling ``WithFlow``: + +:: + + var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) + .WithFlowSqlRepository("Server=localhost;Database=TapetiTest;Integrated Security=true") + .WithFlow() + .RegisterAllControllers() + .Build(); diff --git a/docs/index.rst b/docs/index.rst index 0bc369d..cbc0898 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,4 +10,5 @@ Tapeti documentation indepth dataannotations flow - transient \ No newline at end of file + transient + tapeticmd \ No newline at end of file diff --git a/docs/tapeticmd.rst b/docs/tapeticmd.rst new file mode 100644 index 0000000..c7cc559 --- /dev/null +++ b/docs/tapeticmd.rst @@ -0,0 +1,177 @@ +Tapeti.Cmd +========== + +The Tapeti command-line tool provides various operations for managing messages. It tries to be compatible with all type of messages, but has been tested only against JSON messages, specifically those sent by Tapeti. + + +Common parameters +----------------- + +All operations support the following parameters. All are optional. + +-h , --host + Specifies the hostname of the RabbitMQ server. Default is localhost. + +--port + Specifies the AMQP port of the RabbitMQ server. Default is 5672. + +-v , --virtualhost + Specifies the virtual host to use. Default is /. + +-u , --username + Specifies the username to authenticate the connection. Default is guest. + +-p , --password + Specifies the password to authenticate the connection. Default is guest. + + +Example: +:: + + .\Tapeti.Cmd.exe -h rabbitmq-server -u tapeti -p topsecret + + + +Export +------ + +Fetches messages from a queue and writes them to disk. + +-q , --queue + *Required*. The queue to read the messages from. + +-o , --output + *Required*. Path or filename (depending on the chosen serialization method) where the messages will be output to. + +-r, --remove + If specified messages are acknowledged and removed from the queue. If not messages are kept. + +-n , --maxcount + Maximum number of messages to retrieve from the queue. If not specified all messages are exported. + +-s , --serialization + The method used to serialize the message for import or export. Valid options: SingleFileJSON, EasyNetQHosepipe. Defaults to SingleFileJSON. See Serialization methods below for more information. + + +Example: +:: + + .\Tapeti.Cmd.exe export -q tapeti.example.01 -o dump.json + + + +Import +------ + +Read messages from disk as previously exported and publish them to a queue. + +-i + *Required*. Path or filename (depending on the chosen serialization method) where the messages will be read from. + +-e, --exchange + If specified publishes to the originating exchange using the original routing key. By default these are ignored and the message is published directly to the originating queue. + +-s , --serialization + The method used to serialize the message for import or export. Valid options: SingleFileJSON, EasyNetQHosepipe. Defaults to SingleFileJSON. See Serialization methods below for more information. + + +Example: +:: + + .\Tapeti.Cmd.exe import -i dump.json + + + +Shovel +------ + +Reads messages from a queue and publishes them to another queue, optionally to another RabbitMQ server. + +-q , --queue + *Required*. The queue to read the messages from. + +-t , --targetqueue + The target queue to publish the messages to. Defaults to the source queue if a different target host, port or virtualhost is specified. Otherwise it must be different from the source queue. + +-r, --remove + If specified messages are acknowledged and removed from the queue. If not messages are kept. + +-n , --maxcount + Maximum number of messages to retrieve from the queue. If not specified all messages are exported. + +--targethost + Hostname of the target RabbitMQ server. Defaults to the source host. Note that you may still specify a different targetusername for example. + +--targetport + AMQP port of the target RabbitMQ server. Defaults to the source port. + +--targetvirtualhost + Virtual host used for the target RabbitMQ connection. Defaults to the source virtualhost. + +--targetusername + Username used to connect to the target RabbitMQ server. Defaults to the source username. + +--targetpassword + Password used to connect to the target RabbitMQ server. Defaults to the source password. + + + +Example: +:: + + .\Tapeti.Cmd.exe shovel -q tapeti.example.01 -t tapeti.example.06 + + +Serialization methods +--------------------- + +For importing and exporting messages, Tapeti.Cmd supports two serialization methods. + +SingleFileJSON +'''''''''''''' +The default serialization method. All messages are contained in a single file, where each line is a JSON document describing the message properties and it's content. + +An example message (formatted as multi-line to be more readable, but keep in mind that it must be a single line in the export file to be imported properly): + +:: + + { + "DeliveryTag": 1, + "Redelivered": true, + "Exchange": "tapeti", + "RoutingKey": "quote.request", + "Queue": "tapeti.example.01", + "Properties": { + "AppId": null, + "ClusterId": null, + "ContentEncoding": null, + "ContentType": "application/json", + "CorrelationId": null, + "DeliveryMode": 2, + "Expiration": null, + "Headers": { + "classType": "Messaging.TapetiExample.QuoteRequestMessage:Messaging.TapetiExample" + }, + "MessageId": null, + "Priority": null, + "ReplyTo": null, + "Timestamp": 1581600132, + "Type": null, + "UserId": null + }, + "Body": { + "Amount": 2 + }, + "RawBody": "" + } + +The properties correspond to the RabbitMQ client's IBasicProperties and can be omitted if empty. + +Either Body or RawBody is present. Body is used if the ContentType is set to application/json, and will contain the original message as an inline JSON object for easy manipulation. For other content types, the RawBody contains the original encoded body. + + +EasyNetQHosepipe +'''''''''''''''' +Provides compatibility with the EasyNetQ Hosepipe's dump/insert format. The source or target parameter must be a path. Each message consists of 3 files, ending in .message.txt, .properties.txt and .info.txt. + +As this is only provided for emergency situations, see the source code if you want to know more about the format specification. \ No newline at end of file