1
0
mirror of synced 2024-11-24 11:43:12 +01:00

Implemented stateless request-response support

This commit is contained in:
Mark van Renswoude 2020-02-12 11:34:51 +01:00
parent bbb5f6c218
commit 2745d18779
14 changed files with 1661 additions and 1392 deletions

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RootNamespace>_06_StatelessRequestResponse</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SimpleInjector" Version="4.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Examples\ExampleLib\ExampleLib.csproj" />
<ProjectReference Include="..\Examples\Messaging.TapetiExample\Messaging.TapetiExample.csproj" />
<ProjectReference Include="..\Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj" />
<ProjectReference Include="..\Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj" />
</ItemGroup>
</Project>

View File

@ -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();
}
}
}

View File

@ -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<ILogger, ConsoleLogger>();
var helper = new ExampleConsoleApp(dependencyResolver);
helper.Run(MainAsync);
}
internal static async Task MainAsync(IDependencyResolver dependencyResolver, Func<Task> waitForDone)
{
var config = new TapetiConfig(dependencyResolver)
.WithDataAnnotations()
.RegisterAllControllers()
.Build();
using (var connection = new TapetiConnection(config))
{
await connection.Subscribe();
var publisher = dependencyResolver.Resolve<IPublisher>();
await publisher.PublishRequest<ExampleMessageController, QuoteRequestMessage, QuoteResponseMessage>(
new QuoteRequestMessage
{
Amount = 1
},
c => c.HandleQuoteResponse);
await waitForDone();
}
}
}
}

View File

@ -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
};
}
}
}

View File

@ -0,0 +1,14 @@
using System;
namespace Tapeti.Annotations
{
/// <inheritdoc />
/// <summary>
/// Indicates that the method only handles response messages which are sent directly
/// to the queue. No binding will be created.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ResponseHandlerAttribute : Attribute
{
}
}

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.1</TargetFramework>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<Authors>Mark van Renswoude</Authors> <Authors>Mark van Renswoude</Authors>
<Company>Mark van Renswoude</Company> <Company>Mark van Renswoude</Company>
<Product>Tapeti Command-line Utility</Product> <Product>Tapeti Command-line Utility</Product>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.2" /> <PackageReference Include="RabbitMQ.Client" Version="5.1.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,184 +1,191 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.27703.2026 VisualStudioVersion = 15.0.27703.2026
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Annotations", "Tapeti.Annotations\Tapeti.Annotations.csproj", "{4B742AB2-59DD-4792-8E0F-D80B5366B844}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Annotations", "Tapeti.Annotations\Tapeti.Annotations.csproj", "{4B742AB2-59DD-4792-8E0F-D80B5366B844}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti", "Tapeti\Tapeti.csproj", "{2952B141-C54D-4E6F-8108-CAD735B0279F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations", "Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj", "{6504D430-AB4A-4DE3-AE76-0384591BEEE7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations", "Tapeti.DataAnnotations\Tapeti.DataAnnotations.csproj", "{6504D430-AB4A-4DE3-AE76-0384591BEEE7}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow", "Tapeti.Flow\Tapeti.Flow.csproj", "{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow", "Tapeti.Flow\Tapeti.Flow.csproj", "{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow.SQL", "Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj", "{775CAB72-F443-442E-8E10-313B2548EDF8}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Flow.SQL", "Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj", "{775CAB72-F443-442E-8E10-313B2548EDF8}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.SimpleInjector", "Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj", "{A190C736-E95A-4BDA-AA80-6211226DFCAD}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.SimpleInjector", "Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj", "{A190C736-E95A-4BDA-AA80-6211226DFCAD}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Tests", "Tapeti.Tests\Tapeti.Tests.csproj", "{334F3715-63CF-4D13-B09A-38E2A616D4F5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Tests", "Tapeti.Tests\Tapeti.Tests.csproj", "{334F3715-63CF-4D13-B09A-38E2A616D4F5}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Serilog", "Tapeti.Serilog\Tapeti.Serilog.csproj", "{43AA5DF3-49D5-4795-A290-D6511502B564}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Serilog", "Tapeti.Serilog\Tapeti.Serilog.csproj", "{43AA5DF3-49D5-4795-A290-D6511502B564}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Transient", "Tapeti.Transient\Tapeti.Transient.csproj", "{A6355E63-19AB-47EA-91FA-49B5E9B41F88}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Transient", "Tapeti.Transient\Tapeti.Transient.csproj", "{A6355E63-19AB-47EA-91FA-49B5E9B41F88}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations.Extensions", "Tapeti.DataAnnotations.Extensions\Tapeti.DataAnnotations.Extensions.csproj", "{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.DataAnnotations.Extensions", "Tapeti.DataAnnotations.Extensions\Tapeti.DataAnnotations.Extensions.csproj", "{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{266B9B94-A4D2-41C2-860C-24A7C3B63B56}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{266B9B94-A4D2-41C2-860C-24A7C3B63B56}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01-PublishSubscribe", "Examples\01-PublishSubscribe\01-PublishSubscribe.csproj", "{8350A0AB-F0EE-48CF-9CA6-6019467101CF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "01-PublishSubscribe", "Examples\01-PublishSubscribe\01-PublishSubscribe.csproj", "{8350A0AB-F0EE-48CF-9CA6-6019467101CF}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleLib", "Examples\ExampleLib\ExampleLib.csproj", "{F3B38753-06B4-4932-84B4-A07692AD802D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleLib", "Examples\ExampleLib\ExampleLib.csproj", "{F3B38753-06B4-4932-84B4-A07692AD802D}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.TapetiExample", "Examples\Messaging.TapetiExample\Messaging.TapetiExample.csproj", "{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messaging.TapetiExample", "Examples\Messaging.TapetiExample\Messaging.TapetiExample.csproj", "{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02-DeclareDurableQueues", "Examples\02-DeclareDurableQueues\02-DeclareDurableQueues.csproj", "{85511282-EF91-4B56-B7DC-9E8706556D6E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02-DeclareDurableQueues", "Examples\02-DeclareDurableQueues\02-DeclareDurableQueues.csproj", "{85511282-EF91-4B56-B7DC-9E8706556D6E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03-FlowRequestResponse", "Examples\03-FlowRequestResponse\03-FlowRequestResponse.csproj", "{463A12CE-E221-450D-ADEA-91A599612DFA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03-FlowRequestResponse", "Examples\03-FlowRequestResponse\03-FlowRequestResponse.csproj", "{463A12CE-E221-450D-ADEA-91A599612DFA}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "04-Transient", "Examples\04-Transient\04-Transient.csproj", "{46DFC131-A398-435F-A7DF-3C41B656BF11}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "04-Transient", "Examples\04-Transient\04-Transient.csproj", "{46DFC131-A398-435F-A7DF-3C41B656BF11}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "05-SpeedTest", "Examples\05-SpeedTest\05-SpeedTest.csproj", "{330D05CE-5321-4C7D-8017-2070B891289E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "05-SpeedTest", "Examples\05-SpeedTest\05-SpeedTest.csproj", "{330D05CE-5321-4C7D-8017-2070B891289E}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoC", "IoC", "{99380F97-AD1A-459F-8AB3-D404E1E6AD4F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoC", "IoC", "{99380F97-AD1A-459F-8AB3-D404E1E6AD4F}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8E757FF7-F6D7-42B1-827F-26FA95D97803}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8E757FF7-F6D7-42B1-827F-26FA95D97803}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{57996ADC-18C5-4991-9F95-58D58D442461}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{57996ADC-18C5-4991-9F95-58D58D442461}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.CastleWindsor", "Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj", "{374AAE64-598B-4F67-8870-4A05168FF987}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.CastleWindsor", "Tapeti.CastleWindsor\Tapeti.CastleWindsor.csproj", "{374AAE64-598B-4F67-8870-4A05168FF987}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Autofac", "Tapeti.Autofac\Tapeti.Autofac.csproj", "{B3802005-C941-41B6-A9A5-20573A7C24AE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Autofac", "Tapeti.Autofac\Tapeti.Autofac.csproj", "{B3802005-C941-41B6-A9A5-20573A7C24AE}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.UnityContainer", "Tapeti.UnityContainer\Tapeti.UnityContainer.csproj", "{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.UnityContainer", "Tapeti.UnityContainer\Tapeti.UnityContainer.csproj", "{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Ninject", "Tapeti.Ninject\Tapeti.Ninject.csproj", "{29478B10-FC53-4E93-ADEF-A775D9408131}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Ninject", "Tapeti.Ninject\Tapeti.Ninject.csproj", "{29478B10-FC53-4E93-ADEF-A775D9408131}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{62002327-46B0-4B72-B95A-594CE7F8C80D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{62002327-46B0-4B72-B95A-594CE7F8C80D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Cmd", "Tapeti.Cmd\Tapeti.Cmd.csproj", "{C8728BFC-7F97-41BC-956B-690A57B634EC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tapeti.Cmd", "Tapeti.Cmd\Tapeti.Cmd.csproj", "{C8728BFC-7F97-41BC-956B-690A57B634EC}"
EndProject EndProject
Global Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "06-StatelessRequestResponse", "06-StatelessRequestResponse\06-StatelessRequestResponse.csproj", "{152227AA-3165-4550-8997-6EA80C84516E}"
GlobalSection(SolutionConfigurationPlatforms) = preSolution EndProject
Debug|Any CPU = Debug|Any CPU Global
Release|Any CPU = Release|Any CPU GlobalSection(SolutionConfigurationPlatforms) = preSolution
EndGlobalSection Debug|Any CPU = Debug|Any CPU
GlobalSection(ProjectConfigurationPlatforms) = postSolution Release|Any CPU = Release|Any CPU
{4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU EndGlobalSection
{4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.Build.0 = Debug|Any CPU GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.Build.0 = Release|Any CPU {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B742AB2-59DD-4792-8E0F-D80B5366B844}.Release|Any CPU.Build.0 = Release|Any CPU
{2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.Build.0 = Release|Any CPU {2952B141-C54D-4E6F-8108-CAD735B0279F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU {2952B141-C54D-4E6F-8108-CAD735B0279F}.Release|Any CPU.Build.0 = Release|Any CPU
{6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.Build.0 = Release|Any CPU {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.Build.0 = Debug|Any CPU {6504D430-AB4A-4DE3-AE76-0384591BEEE7}.Release|Any CPU.Build.0 = Release|Any CPU
{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.ActiveCfg = Release|Any CPU {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.Build.0 = Release|Any CPU {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {14CF8F01-570B-4B84-AB4A-E0C3EC117F89}.Release|Any CPU.Build.0 = Release|Any CPU
{775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.Build.0 = Release|Any CPU {775CAB72-F443-442E-8E10-313B2548EDF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU {775CAB72-F443-442E-8E10-313B2548EDF8}.Release|Any CPU.Build.0 = Release|Any CPU
{A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.Build.0 = Release|Any CPU {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A190C736-E95A-4BDA-AA80-6211226DFCAD}.Release|Any CPU.Build.0 = Release|Any CPU
{334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.Build.0 = Release|Any CPU {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.Build.0 = Debug|Any CPU {334F3715-63CF-4D13-B09A-38E2A616D4F5}.Release|Any CPU.Build.0 = Release|Any CPU
{43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.ActiveCfg = Release|Any CPU {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.Build.0 = Release|Any CPU {43AA5DF3-49D5-4795-A290-D6511502B564}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.Build.0 = Debug|Any CPU {43AA5DF3-49D5-4795-A290-D6511502B564}.Release|Any CPU.Build.0 = Release|Any CPU
{A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.Build.0 = Release|Any CPU {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6355E63-19AB-47EA-91FA-49B5E9B41F88}.Release|Any CPU.Build.0 = Release|Any CPU
{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.Build.0 = Release|Any CPU {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831}.Release|Any CPU.Build.0 = Release|Any CPU
{8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.Build.0 = Release|Any CPU {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.Build.0 = Debug|Any CPU {8350A0AB-F0EE-48CF-9CA6-6019467101CF}.Release|Any CPU.Build.0 = Release|Any CPU
{F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.Build.0 = Release|Any CPU {F3B38753-06B4-4932-84B4-A07692AD802D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3B38753-06B4-4932-84B4-A07692AD802D}.Release|Any CPU.Build.0 = Release|Any CPU
{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.Build.0 = Release|Any CPU {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84}.Release|Any CPU.Build.0 = Release|Any CPU
{85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.Build.0 = Release|Any CPU {85511282-EF91-4B56-B7DC-9E8706556D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {85511282-EF91-4B56-B7DC-9E8706556D6E}.Release|Any CPU.Build.0 = Release|Any CPU
{463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.Build.0 = Release|Any CPU {463A12CE-E221-450D-ADEA-91A599612DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.Build.0 = Debug|Any CPU {463A12CE-E221-450D-ADEA-91A599612DFA}.Release|Any CPU.Build.0 = Release|Any CPU
{46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.ActiveCfg = Release|Any CPU {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.Build.0 = Release|Any CPU {46DFC131-A398-435F-A7DF-3C41B656BF11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.Build.0 = Debug|Any CPU {46DFC131-A398-435F-A7DF-3C41B656BF11}.Release|Any CPU.Build.0 = Release|Any CPU
{330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.ActiveCfg = Release|Any CPU {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.Build.0 = Release|Any CPU {330D05CE-5321-4C7D-8017-2070B891289E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.Build.0 = Debug|Any CPU {330D05CE-5321-4C7D-8017-2070B891289E}.Release|Any CPU.Build.0 = Release|Any CPU
{374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.ActiveCfg = Release|Any CPU {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.Build.0 = Release|Any CPU {374AAE64-598B-4F67-8870-4A05168FF987}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {374AAE64-598B-4F67-8870-4A05168FF987}.Release|Any CPU.Build.0 = Release|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.Build.0 = Release|Any CPU {B3802005-C941-41B6-A9A5-20573A7C24AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3802005-C941-41B6-A9A5-20573A7C24AE}.Release|Any CPU.Build.0 = Release|Any CPU
{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.Build.0 = Release|Any CPU {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E}.Release|Any CPU.Build.0 = Release|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.ActiveCfg = Release|Any CPU {29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.Build.0 = Release|Any CPU {29478B10-FC53-4E93-ADEF-A775D9408131}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {29478B10-FC53-4E93-ADEF-A775D9408131}.Release|Any CPU.Build.0 = Release|Any CPU
{C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.Build.0 = Release|Any CPU {C8728BFC-7F97-41BC-956B-690A57B634EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
GlobalSection(SolutionProperties) = preSolution {C8728BFC-7F97-41BC-956B-690A57B634EC}.Release|Any CPU.Build.0 = Release|Any CPU
HideSolutionNode = FALSE {152227AA-3165-4550-8997-6EA80C84516E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
EndGlobalSection {152227AA-3165-4550-8997-6EA80C84516E}.Debug|Any CPU.Build.0 = Debug|Any CPU
GlobalSection(NestedProjects) = preSolution {152227AA-3165-4550-8997-6EA80C84516E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B742AB2-59DD-4792-8E0F-D80B5366B844} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} {152227AA-3165-4550-8997-6EA80C84516E}.Release|Any CPU.Build.0 = Release|Any CPU
{2952B141-C54D-4E6F-8108-CAD735B0279F} = {8E757FF7-F6D7-42B1-827F-26FA95D97803} EndGlobalSection
{6504D430-AB4A-4DE3-AE76-0384591BEEE7} = {57996ADC-18C5-4991-9F95-58D58D442461} GlobalSection(SolutionProperties) = preSolution
{14CF8F01-570B-4B84-AB4A-E0C3EC117F89} = {57996ADC-18C5-4991-9F95-58D58D442461} HideSolutionNode = FALSE
{775CAB72-F443-442E-8E10-313B2548EDF8} = {57996ADC-18C5-4991-9F95-58D58D442461} EndGlobalSection
{A190C736-E95A-4BDA-AA80-6211226DFCAD} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} GlobalSection(NestedProjects) = preSolution
{43AA5DF3-49D5-4795-A290-D6511502B564} = {57996ADC-18C5-4991-9F95-58D58D442461} {4B742AB2-59DD-4792-8E0F-D80B5366B844} = {8E757FF7-F6D7-42B1-827F-26FA95D97803}
{A6355E63-19AB-47EA-91FA-49B5E9B41F88} = {57996ADC-18C5-4991-9F95-58D58D442461} {2952B141-C54D-4E6F-8108-CAD735B0279F} = {8E757FF7-F6D7-42B1-827F-26FA95D97803}
{1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831} = {57996ADC-18C5-4991-9F95-58D58D442461} {6504D430-AB4A-4DE3-AE76-0384591BEEE7} = {57996ADC-18C5-4991-9F95-58D58D442461}
{8350A0AB-F0EE-48CF-9CA6-6019467101CF} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {14CF8F01-570B-4B84-AB4A-E0C3EC117F89} = {57996ADC-18C5-4991-9F95-58D58D442461}
{F3B38753-06B4-4932-84B4-A07692AD802D} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {775CAB72-F443-442E-8E10-313B2548EDF8} = {57996ADC-18C5-4991-9F95-58D58D442461}
{D24120D4-50A2-44B6-A4EA-6ADAAEBABA84} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {A190C736-E95A-4BDA-AA80-6211226DFCAD} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
{85511282-EF91-4B56-B7DC-9E8706556D6E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {43AA5DF3-49D5-4795-A290-D6511502B564} = {57996ADC-18C5-4991-9F95-58D58D442461}
{463A12CE-E221-450D-ADEA-91A599612DFA} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {A6355E63-19AB-47EA-91FA-49B5E9B41F88} = {57996ADC-18C5-4991-9F95-58D58D442461}
{46DFC131-A398-435F-A7DF-3C41B656BF11} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {1AAA5A2C-EAA8-4C49-96A6-673EA1EEE831} = {57996ADC-18C5-4991-9F95-58D58D442461}
{330D05CE-5321-4C7D-8017-2070B891289E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56} {8350A0AB-F0EE-48CF-9CA6-6019467101CF} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{374AAE64-598B-4F67-8870-4A05168FF987} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} {F3B38753-06B4-4932-84B4-A07692AD802D} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{B3802005-C941-41B6-A9A5-20573A7C24AE} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} {D24120D4-50A2-44B6-A4EA-6ADAAEBABA84} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} {85511282-EF91-4B56-B7DC-9E8706556D6E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{29478B10-FC53-4E93-ADEF-A775D9408131} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F} {463A12CE-E221-450D-ADEA-91A599612DFA} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
{C8728BFC-7F97-41BC-956B-690A57B634EC} = {62002327-46B0-4B72-B95A-594CE7F8C80D} {46DFC131-A398-435F-A7DF-3C41B656BF11} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
EndGlobalSection {330D05CE-5321-4C7D-8017-2070B891289E} = {266B9B94-A4D2-41C2-860C-24A7C3B63B56}
GlobalSection(ExtensibilityGlobals) = postSolution {374AAE64-598B-4F67-8870-4A05168FF987} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
SolutionGuid = {B09CC2BF-B2AF-4CB6-8728-5D1D8E5C50FA} {B3802005-C941-41B6-A9A5-20573A7C24AE} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
EndGlobalSection {BA8CA9A2-BAFF-42BB-8439-3DD9D1F6C32E} = {99380F97-AD1A-459F-8AB3-D404E1E6AD4F}
EndGlobal {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

View File

@ -1,112 +1,120 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
namespace Tapeti.Config
{ namespace Tapeti.Config
/// <summary> {
/// Provides access to the Tapeti configuration. /// <summary>
/// </summary> /// Provides access to the Tapeti configuration.
public interface ITapetiConfig /// </summary>
{ public interface ITapetiConfig
/// <summary> {
/// Reference to the wrapper for an IoC container, to provide dependency injection to Tapeti. /// <summary>
/// </summary> /// Reference to the wrapper for an IoC container, to provide dependency injection to Tapeti.
IDependencyResolver DependencyResolver { get; } /// </summary>
IDependencyResolver DependencyResolver { get; }
/// <summary>
/// Various Tapeti features which can be turned on or off. /// <summary>
/// </summary> /// Various Tapeti features which can be turned on or off.
ITapetiConfigFeatues Features { get; } /// </summary>
ITapetiConfigFeatues Features { get; }
/// <summary>
/// Provides access to the different kinds of registered middleware. /// <summary>
/// </summary> /// Provides access to the different kinds of registered middleware.
ITapetiConfigMiddleware Middleware { get; } /// </summary>
ITapetiConfigMiddleware Middleware { get; }
/// <summary>
/// A list of all registered bindings. /// <summary>
/// </summary> /// A list of all registered bindings.
ITapetiConfigBindings Bindings { get; } /// </summary>
} ITapetiConfigBindings Bindings { get; }
}
/// <summary>
/// Various Tapeti features which can be turned on or off. /// <summary>
/// </summary> /// Various Tapeti features which can be turned on or off.
public interface ITapetiConfigFeatues /// </summary>
{ public interface ITapetiConfigFeatues
/// <summary> {
/// Determines whether 'publisher confirms' are used. This RabbitMQ features allows Tapeti to /// <summary>
/// be notified if a message has no route, and guarantees delivery for request-response style /// Determines whether 'publisher confirms' are used. This RabbitMQ features allows Tapeti to
/// messages and those marked with the Mandatory attribute. On by default, can only be turned /// be notified if a message has no route, and guarantees delivery for request-response style
/// off by explicitly calling DisablePublisherConfirms, which is not recommended. /// messages and those marked with the Mandatory attribute. On by default, can only be turned
/// </summary> /// off by explicitly calling DisablePublisherConfirms, which is not recommended.
bool PublisherConfirms { get; } /// </summary>
bool PublisherConfirms { get; }
/// <summary>
/// If enabled, durable queues will be created at startup and their bindings will be updated /// <summary>
/// with the currently registered message handlers. If not enabled all durable queues must /// If enabled, durable queues will be created at startup and their bindings will be updated
/// already be present when the connection is made. /// with the currently registered message handlers. If not enabled all durable queues must
/// </summary> /// already be present when the connection is made.
bool DeclareDurableQueues { get; } /// </summary>
} bool DeclareDurableQueues { get; }
}
/// <summary>
/// Provides access to the different kinds of registered middleware. /// <summary>
/// </summary> /// Provides access to the different kinds of registered middleware.
public interface ITapetiConfigMiddleware /// </summary>
{ public interface ITapetiConfigMiddleware
/// <summary> {
/// A list of message middleware which is called when a message is being consumed. /// <summary>
/// </summary> /// A list of message middleware which is called when a message is being consumed.
IReadOnlyList<IMessageMiddleware> Message { get; } /// </summary>
IReadOnlyList<IMessageMiddleware> Message { get; }
/// <summary>
/// A list of publish middleware which is called when a message is being published. /// <summary>
/// </summary> /// A list of publish middleware which is called when a message is being published.
IReadOnlyList<IPublishMiddleware> Publish { get; } /// </summary>
} IReadOnlyList<IPublishMiddleware> Publish { get; }
}
/// <inheritdoc />
/// <summary> /// <inheritdoc />
/// Contains a list of registered bindings, with a few added helpers. /// <summary>
/// </summary> /// Contains a list of registered bindings, with a few added helpers.
public interface ITapetiConfigBindings : IReadOnlyList<IBinding> /// </summary>
{ public interface ITapetiConfigBindings : IReadOnlyList<IBinding>
/// <summary> {
/// Searches for a binding linked to the specified method. /// <summary>
/// </summary> /// Searches for a binding linked to the specified method.
/// <param name="method"></param> /// </summary>
/// <returns>The binding if found, null otherwise</returns> /// <param name="method"></param>
IControllerMethodBinding ForMethod(Delegate method); /// <returns>The binding if found, null otherwise</returns>
} IControllerMethodBinding ForMethod(Delegate method);
/// <summary>
/* /// Searches for a binding linked to the specified method.
public interface IBinding /// </summary>
{ /// <param name="method"></param>
Type Controller { get; } /// <returns>The binding if found, null otherwise</returns>
MethodInfo Method { get; } IControllerMethodBinding ForMethod(MethodInfo method);
Type MessageClass { get; } }
string QueueName { get; }
QueueBindingMode QueueBindingMode { get; set; }
/*
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; } public interface IBinding
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; } {
Type Controller { get; }
bool Accept(Type messageClass); MethodInfo Method { get; }
bool Accept(IMessageContext context, object message); Type MessageClass { get; }
Task Invoke(IMessageContext context, object message); string QueueName { get; }
} QueueBindingMode QueueBindingMode { get; set; }
*/
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
/*
public interface IBuildBinding : IBinding bool Accept(Type messageClass);
{ bool Accept(IMessageContext context, object message);
void SetQueueName(string queueName); Task Invoke(IMessageContext context, object message);
} }
*/ */
}
/*
public interface IBuildBinding : IBinding
{
void SetQueueName(string queueName);
}
*/
}

View File

@ -1,104 +1,163 @@
using System; using System;
using System.Reflection; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Reflection;
using Tapeti.Annotations; using System.Threading.Tasks;
using Tapeti.Config; using Tapeti.Annotations;
using Tapeti.Default; using Tapeti.Config;
using Tapeti.Helpers; using Tapeti.Default;
using Tapeti.Helpers;
namespace Tapeti.Connection
{ namespace Tapeti.Connection
/// <inheritdoc /> {
internal class TapetiPublisher : IInternalPublisher /// <inheritdoc />
{ internal class TapetiPublisher : IInternalPublisher
private readonly ITapetiConfig config; {
private readonly Func<ITapetiClient> clientFactory; private readonly ITapetiConfig config;
private readonly IExchangeStrategy exchangeStrategy; private readonly Func<ITapetiClient> clientFactory;
private readonly IRoutingKeyStrategy routingKeyStrategy; private readonly IExchangeStrategy exchangeStrategy;
private readonly IMessageSerializer messageSerializer; private readonly IRoutingKeyStrategy routingKeyStrategy;
private readonly IMessageSerializer messageSerializer;
/// <inheritdoc />
public TapetiPublisher(ITapetiConfig config, Func<ITapetiClient> clientFactory) public TapetiPublisher(ITapetiConfig config, Func<ITapetiClient> clientFactory)
{ {
this.config = config; this.config = config;
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>(); exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>();
routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>(); routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>();
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>(); messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Publish(object message) public async Task Publish(object message)
{ {
await Publish(message, null, IsMandatory(message)); await Publish(message, null, IsMandatory(message));
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Publish(object message, IMessageProperties properties, bool mandatory) public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class
{ {
var messageClass = message.GetType(); await PublishRequest(message, responseMethodSelector.Body);
var exchange = exchangeStrategy.GetExchange(messageClass); }
var routingKey = routingKeyStrategy.GetRoutingKey(messageClass);
await Publish(message, properties, exchange, routingKey, mandatory); /// <inheritdoc />
} public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class
{
await PublishRequest(message, responseMethodSelector.Body);
/// <inheritdoc /> }
public async Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory)
{
await Publish(message, properties, null, queueName, mandatory); private async Task PublishRequest(object message, Expression responseMethodBody)
} {
var callExpression = (responseMethodBody as UnaryExpression)?.Operand as MethodCallExpression;
var targetMethodExpression = callExpression?.Object as ConstantExpression;
private async Task Publish(object message, IMessageProperties properties, string exchange, string routingKey, bool mandatory)
{ var responseHandler = targetMethodExpression?.Value as MethodInfo;
var writableProperties = new MessageProperties(properties); if (responseHandler == null)
throw new ArgumentException("Unable to determine the response method", nameof(responseMethodBody));
if (!writableProperties.Timestamp.HasValue)
writableProperties.Timestamp = DateTime.UtcNow;
var requestAttribute = message.GetType().GetCustomAttribute<RequestAttribute>();
writableProperties.Persistent = true; 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 context = new PublishContext var binding = config.Bindings.ForMethod(responseHandler);
{ if (binding == null)
Config = config, throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler));
Exchange = exchange,
RoutingKey = routingKey, if (!binding.Accept(requestAttribute.Response))
Message = message, throw new ArgumentException($"responseHandler must accept message of type {requestAttribute.Response}", nameof(responseHandler));
Properties = writableProperties
}; var responseHandleAttribute = binding.Method.GetCustomAttribute<ResponseHandlerAttribute>();
if (responseHandleAttribute == null)
throw new ArgumentException("responseHandler must be marked with the ResponseHandler attribute", nameof(responseHandler));
await MiddlewareHelper.GoAsync(
config.Middleware.Publish, if (binding.QueueName == null)
async (handler, next) => await handler.Handle(context, next), throw new ArgumentException("responseHandler is not yet subscribed to a queue, TapetiConnection.Subscribe must be called before starting a request", nameof(responseHandler));
async () =>
{
var body = messageSerializer.Serialize(message, writableProperties); var properties = new MessageProperties
await clientFactory().Publish(body, writableProperties, exchange, routingKey, mandatory); {
}); ReplyTo = binding.QueueName
} };
await Publish(message, properties, IsMandatory(message));
private static bool IsMandatory(object message) }
{
return message.GetType().GetCustomAttribute<MandatoryAttribute>() != null;
} /// <inheritdoc />
public async Task SendToQueue(string queueName, object message)
{
private class PublishContext : IPublishContext await PublishDirect(message, queueName, null, IsMandatory(message));
{ }
public ITapetiConfig Config { get; set; }
public string Exchange { get; set; }
public string RoutingKey { get; set; } /// <inheritdoc />
public object Message { get; set; } public async Task Publish(object message, IMessageProperties properties, bool mandatory)
public IMessageProperties Properties { get; set; } {
} var messageClass = message.GetType();
} var exchange = exchangeStrategy.GetExchange(messageClass);
} var routingKey = routingKeyStrategy.GetRoutingKey(messageClass);
await Publish(message, properties, exchange, routingKey, mandatory);
}
/// <inheritdoc />
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<MandatoryAttribute>() != 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; }
}
}
}

View File

@ -1,175 +1,174 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Tapeti.Config; using Tapeti.Config;
namespace Tapeti.Default namespace Tapeti.Default
{ {
internal class ControllerBindingContext : IControllerBindingContext internal class ControllerBindingContext : IControllerBindingContext
{ {
private BindingTargetMode? bindingTargetMode; private BindingTargetMode? bindingTargetMode;
private readonly List<IControllerMiddlewareBase> middleware = new List<IControllerMiddlewareBase>(); private readonly List<IControllerMiddlewareBase> middleware = new List<IControllerMiddlewareBase>();
private readonly List<ControllerBindingParameter> parameters; private readonly List<ControllerBindingParameter> parameters;
private readonly ControllerBindingResult result; private readonly ControllerBindingResult result;
/// <summary> /// <summary>
/// Determines how the binding target is configured. /// Determines how the binding target is configured.
/// </summary> /// </summary>
public BindingTargetMode BindingTargetMode => bindingTargetMode ?? BindingTargetMode.Default; public BindingTargetMode BindingTargetMode => bindingTargetMode ?? BindingTargetMode.Default;
/// <summary> /// <summary>
/// Provides access to the registered middleware for this method. /// Provides access to the registered middleware for this method.
/// </summary> /// </summary>
public IReadOnlyList<IControllerMiddlewareBase> Middleware => middleware; public IReadOnlyList<IControllerMiddlewareBase> Middleware => middleware;
/// <inheritdoc /> /// <inheritdoc />
public Type MessageClass { get; set; } public Type MessageClass { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool HasMessageClass => MessageClass != null; public bool HasMessageClass => MessageClass != null;
/// <inheritdoc /> /// <inheritdoc />
public Type Controller { get; set; } public Type Controller { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public MethodInfo Method { get; set; } public MethodInfo Method { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<IBindingParameter> Parameters => parameters; public IReadOnlyList<IBindingParameter> Parameters => parameters;
/// <inheritdoc /> /// <inheritdoc />
public IBindingResult Result => result; public IBindingResult Result => result;
/// <inheritdoc /> public ControllerBindingContext(IEnumerable<ParameterInfo> parameters, ParameterInfo result)
public ControllerBindingContext(IEnumerable<ParameterInfo> parameters, ParameterInfo result) {
{ this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList();
this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList();
this.result = new ControllerBindingResult(result);
this.result = new ControllerBindingResult(result); }
}
/// <inheritdoc />
/// <inheritdoc /> public void SetMessageClass(Type messageClass)
public void SetMessageClass(Type messageClass) {
{ if (HasMessageClass)
if (HasMessageClass) throw new InvalidOperationException("SetMessageClass can only be called once");
throw new InvalidOperationException("SetMessageClass can only be called once");
MessageClass = messageClass;
MessageClass = messageClass; }
}
/// <inheritdoc />
/// <inheritdoc /> public void SetBindingTargetMode(BindingTargetMode mode)
public void SetBindingTargetMode(BindingTargetMode mode) {
{ if (bindingTargetMode.HasValue)
if (bindingTargetMode.HasValue) throw new InvalidOperationException("SetBindingTargetMode can only be called once");
throw new InvalidOperationException("SetBindingTargetMode can only be called once");
bindingTargetMode = mode;
bindingTargetMode = mode; }
}
/// <inheritdoc />
/// <inheritdoc /> public void Use(IControllerMiddlewareBase handler)
public void Use(IControllerMiddlewareBase handler) {
{ middleware.Add(handler);
middleware.Add(handler); }
}
/// <summary>
/// <summary> /// Returns the configured bindings for the parameters.
/// Returns the configured bindings for the parameters. /// </summary>
/// </summary> public IEnumerable<ValueFactory> GetParameterHandlers()
public IEnumerable<ValueFactory> GetParameterHandlers() {
{ return parameters.Select(p => p.Binding);
return parameters.Select(p => p.Binding); }
}
/// <summary>
/// <summary> /// Returns the configured result handler.
/// Returns the configured result handler. /// </summary>
/// </summary> /// <returns></returns>
/// <returns></returns> public ResultHandler GetResultHandler()
public ResultHandler GetResultHandler() {
{ return result.Handler;
return result.Handler; }
} }
}
/// <inheritdoc />
/// <inheritdoc /> /// <summary>
/// <summary> /// Default implementation for IBindingParameter
/// Default implementation for IBindingParameter /// </summary>
/// </summary> public class ControllerBindingParameter : IBindingParameter
public class ControllerBindingParameter : IBindingParameter {
{ /// <summary>
/// <summary> /// Provides access to the configured binding.
/// Provides access to the configured binding. /// </summary>
/// </summary> public ValueFactory Binding { get; set; }
public ValueFactory Binding { get; set; }
/// <inheritdoc />
/// <inheritdoc /> public ParameterInfo Info { get; }
public ParameterInfo Info { get; }
/// <inheritdoc />
/// <inheritdoc /> public bool HasBinding => Binding != null;
public bool HasBinding => Binding != null;
/// <inheritdoc />
/// <inheritdoc /> public ControllerBindingParameter(ParameterInfo info)
public ControllerBindingParameter(ParameterInfo info) {
{ Info = info;
Info = info; }
}
/// <inheritdoc />
/// <inheritdoc /> public void SetBinding(ValueFactory valueFactory)
public void SetBinding(ValueFactory valueFactory) {
{ if (Binding != null)
if (Binding != null) throw new InvalidOperationException("SetBinding can only be called once");
throw new InvalidOperationException("SetBinding can only be called once");
Binding = valueFactory;
Binding = valueFactory; }
} }
}
/// <inheritdoc />
/// <inheritdoc /> /// <summary>
/// <summary> /// Default implementation for IBindingResult
/// Default implementation for IBindingResult /// </summary>
/// </summary> public class ControllerBindingResult : IBindingResult
public class ControllerBindingResult : IBindingResult {
{ /// <summary>
/// <summary> /// Provides access to the configured handler.
/// Provides access to the configured handler. /// </summary>
/// </summary> public ResultHandler Handler { get; set; }
public ResultHandler Handler { get; set; }
/// <inheritdoc />
/// <inheritdoc /> public ParameterInfo Info { get; }
public ParameterInfo Info { get; }
/// <inheritdoc />
/// <inheritdoc /> public bool HasHandler => Handler != null;
public bool HasHandler => Handler != null;
/// <inheritdoc />
/// <inheritdoc /> public ControllerBindingResult(ParameterInfo info)
public ControllerBindingResult(ParameterInfo info) {
{ Info = info;
Info = info; }
}
/// <inheritdoc />
/// <inheritdoc /> public void SetHandler(ResultHandler resultHandler)
public void SetHandler(ResultHandler resultHandler) {
{ if (Handler != null)
if (Handler != null) throw new InvalidOperationException("SetHandler can only be called once");
throw new InvalidOperationException("SetHandler can only be called once");
Handler = resultHandler;
Handler = resultHandler; }
} }
} }
}

View File

@ -1,50 +1,86 @@
using System.Threading.Tasks; using System;
using Tapeti.Config; using System.Linq.Expressions;
using System.Threading.Tasks;
// ReSharper disable once UnusedMember.Global using Tapeti.Config;
namespace Tapeti // ReSharper disable once UnusedMember.Global
{
/// <summary> namespace Tapeti
/// Allows publishing of messages. {
/// </summary> /// <summary>
public interface IPublisher /// Allows publishing of messages.
{ /// </summary>
/// <summary> public interface IPublisher
/// Publish the specified message. Transport details are determined by the Tapeti configuration. {
/// </summary> /// <summary>
/// <param name="message">The message to send</param> /// Publish the specified message. Transport details are determined by the Tapeti configuration.
Task Publish(object message); /// </summary>
} /// <param name="message">The message to send</param>
Task Publish(object message);
/// <inheritdoc />
/// <summary> /// <summary>
/// Low-level publisher for Tapeti internal use. /// Publish the specified request message and handle the response with the controller method as specified
/// </summary> /// by the responseMethodSelector expression. The response method or controller must have a valid queue attribute.
/// <remarks> /// </summary>
/// Tapeti assumes every implementation of IPublisher can also be cast to an IInternalPublisher. /// <remarks>
/// The distinction is made on purpose to trigger code-smells in non-Tapeti code when casting. /// The response method is called on a new instance of the controller, as is the case with a regular message.
/// </remarks> /// To preserve state, use the Tapeti.Flow extension instead.
public interface IInternalPublisher : IPublisher /// </remarks>
{ /// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param>
/// <summary> /// <param name="message">The message to send</param>
/// Publishes a message. The exchange and routing key are determined by the registered strategies. Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class;
/// </summary>
/// <param name="message">An instance of a message class</param>
/// <param name="properties">Metadata to include in the message</param> /// <summary>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param> /// Publish the specified request message and handle the response with the controller method as specified
Task Publish(object message, IMessageProperties properties, bool mandatory); /// by the responseMethodSelector expression. The response method or controller must have a valid queue attribute.
/// </summary>
/// <remarks>
/// <summary> /// The response method is called on a new instance of the controller, as is the case with a regular message.
/// Publishes a message directly to a queue. The exchange and routing key are not used. /// To preserve state, use the Tapeti.Flow extension instead.
/// </summary> /// </remarks>
/// <param name="message">An instance of a message class</param> /// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param>
/// <param name="queueName">The name of the queue to send the message to</param> /// <param name="message">The message to send</param>
/// <param name="properties">Metadata to include in the message</param> Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class;
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to the queue</param>
/// <returns></returns>
Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory); /// <summary>
} /// Sends a message directly to the specified queue. Not recommended for general use.
} /// </summary>
/// <param name="queueName">The name of the queue to publish the message to</param>
/// <param name="message">The message to send</param>
Task SendToQueue(string queueName, object message);
}
/// <inheritdoc />
/// <summary>
/// Low-level publisher for Tapeti internal use.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public interface IInternalPublisher : IPublisher
{
/// <summary>
/// Publishes a message. The exchange and routing key are determined by the registered strategies.
/// </summary>
/// <param name="message">An instance of a message class</param>
/// <param name="properties">Metadata to include in the message</param>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param>
Task Publish(object message, IMessageProperties properties, bool mandatory);
/// <summary>
/// Publishes a message directly to a queue. The exchange and routing key are not used.
/// </summary>
/// <param name="message">An instance of a message class</param>
/// <param name="queueName">The name of the queue to send the message to</param>
/// <param name="properties">Metadata to include in the message</param>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to the queue</param>
/// <returns></returns>
Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory);
}
}

View File

@ -1,311 +1,317 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Default; using Tapeti.Default;
using Tapeti.Helpers; using Tapeti.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
namespace Tapeti namespace Tapeti
{ {
/// <inheritdoc cref="ITapetiConfigBuilder" /> /// <inheritdoc cref="ITapetiConfigBuilder" />
/// <summary> /// <summary>
/// Default implementation of the Tapeti config builder. /// Default implementation of the Tapeti config builder.
/// Automatically registers the default middleware for injecting the message parameter and handling the return value. /// Automatically registers the default middleware for injecting the message parameter and handling the return value.
/// </summary> /// </summary>
public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess
{ {
private Config config; private Config config;
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>(); private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>();
/// <inheritdoc /> /// <inheritdoc />
public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver; public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver;
/// <summary> /// <summary>
/// Instantiates a new Tapeti config builder. /// Instantiates a new Tapeti config builder.
/// </summary> /// </summary>
/// <param name="dependencyResolver">A wrapper implementation for an IoC container to allow dependency injection</param> /// <param name="dependencyResolver">A wrapper implementation for an IoC container to allow dependency injection</param>
public TapetiConfig(IDependencyResolver dependencyResolver) public TapetiConfig(IDependencyResolver dependencyResolver)
{ {
config = new Config(dependencyResolver); config = new Config(dependencyResolver);
Use(new DependencyResolverBinding()); Use(new DependencyResolverBinding());
Use(new PublishResultBinding()); Use(new PublishResultBinding());
// Registered last so it runs first and the MessageClass is known to other middleware // Registered last so it runs first and the MessageClass is known to other middleware
Use(new MessageBinding()); Use(new MessageBinding());
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfig Build() public ITapetiConfig Build()
{ {
if (config == null) if (config == null)
throw new InvalidOperationException("TapetiConfig.Build must only be called once"); throw new InvalidOperationException("TapetiConfig.Build must only be called once");
RegisterDefaults(); RegisterDefaults();
(config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<ITapetiConfig>(config); (config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<ITapetiConfig>(config);
var outputConfig = config; var outputConfig = config;
config = null; config = null;
outputConfig.Lock(); outputConfig.Lock();
return outputConfig; return outputConfig;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler) public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler)
{ {
bindingMiddleware.Add(handler); bindingMiddleware.Add(handler);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder Use(IMessageMiddleware handler) public ITapetiConfigBuilder Use(IMessageMiddleware handler)
{ {
GetConfig().Use(handler); GetConfig().Use(handler);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder Use(IPublishMiddleware handler) public ITapetiConfigBuilder Use(IPublishMiddleware handler)
{ {
GetConfig().Use(handler); GetConfig().Use(handler);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder Use(ITapetiExtension extension) public ITapetiConfigBuilder Use(ITapetiExtension extension)
{ {
if (DependencyResolver is IDependencyContainer container) if (DependencyResolver is IDependencyContainer container)
extension.RegisterDefaults(container); extension.RegisterDefaults(container);
var configInstance = GetConfig(); var configInstance = GetConfig();
var middlewareBundle = extension.GetMiddleware(DependencyResolver); var middlewareBundle = extension.GetMiddleware(DependencyResolver);
if (middlewareBundle != null) if (middlewareBundle != null)
{ {
foreach (var middleware in middlewareBundle) foreach (var middleware in middlewareBundle)
{ {
switch (middleware) switch (middleware)
{ {
case IControllerBindingMiddleware bindingExtension: case IControllerBindingMiddleware bindingExtension:
Use(bindingExtension); Use(bindingExtension);
break; break;
case IMessageMiddleware messageExtension: case IMessageMiddleware messageExtension:
configInstance.Use(messageExtension); configInstance.Use(messageExtension);
break; break;
case IPublishMiddleware publishExtension: case IPublishMiddleware publishExtension:
configInstance.Use(publishExtension); configInstance.Use(publishExtension);
break; break;
default: default:
throw new ArgumentException( throw new ArgumentException(
$"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}"); $"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}");
} }
} }
} }
var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver); var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver);
if (bindingBundle == null) if (bindingBundle == null)
return this; return this;
foreach (var binding in bindingBundle) foreach (var binding in bindingBundle)
config.RegisterBinding(binding); config.RegisterBinding(binding);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public void RegisterBinding(IBinding binding) public void RegisterBinding(IBinding binding)
{ {
GetConfig().RegisterBinding(binding); GetConfig().RegisterBinding(binding);
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder DisablePublisherConfirms() public ITapetiConfigBuilder DisablePublisherConfirms()
{ {
GetConfig().SetPublisherConfirms(false); GetConfig().SetPublisherConfirms(false);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder SetPublisherConfirms(bool enabled) public ITapetiConfigBuilder SetPublisherConfirms(bool enabled)
{ {
GetConfig().SetPublisherConfirms(enabled); GetConfig().SetPublisherConfirms(enabled);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder EnableDeclareDurableQueues() public ITapetiConfigBuilder EnableDeclareDurableQueues()
{ {
GetConfig().SetDeclareDurableQueues(true); GetConfig().SetDeclareDurableQueues(true);
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled) public ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled)
{ {
GetConfig().SetDeclareDurableQueues(enabled); GetConfig().SetDeclareDurableQueues(enabled);
return this; return this;
} }
/// <summary> /// <summary>
/// Registers the default implementation of various Tapeti interfaces into the IoC container. /// Registers the default implementation of various Tapeti interfaces into the IoC container.
/// </summary> /// </summary>
protected void RegisterDefaults() protected void RegisterDefaults()
{ {
if (!(DependencyResolver is IDependencyContainer container)) if (!(DependencyResolver is IDependencyContainer container))
return; return;
if (ConsoleHelper.IsAvailable()) if (ConsoleHelper.IsAvailable())
container.RegisterDefault<ILogger, ConsoleLogger>(); container.RegisterDefault<ILogger, ConsoleLogger>();
else else
container.RegisterDefault<ILogger, DevNullLogger>(); container.RegisterDefault<ILogger, DevNullLogger>();
container.RegisterDefault<IMessageSerializer, JsonMessageSerializer>(); container.RegisterDefault<IMessageSerializer, JsonMessageSerializer>();
container.RegisterDefault<IExchangeStrategy, NamespaceMatchExchangeStrategy>(); container.RegisterDefault<IExchangeStrategy, NamespaceMatchExchangeStrategy>();
container.RegisterDefault<IRoutingKeyStrategy, TypeNameRoutingKeyStrategy>(); container.RegisterDefault<IRoutingKeyStrategy, TypeNameRoutingKeyStrategy>();
container.RegisterDefault<IExceptionStrategy, NackExceptionStrategy>(); container.RegisterDefault<IExceptionStrategy, NackExceptionStrategy>();
} }
/// <inheritdoc /> /// <inheritdoc />
public void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler) public void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler)
{ {
MiddlewareHelper.Go(bindingMiddleware, MiddlewareHelper.Go(bindingMiddleware,
(handler, next) => handler.Handle(context, next), (handler, next) => handler.Handle(context, next),
lastHandler); lastHandler);
} }
private Config GetConfig() private Config GetConfig()
{ {
if (config == null) if (config == null)
throw new InvalidOperationException("TapetiConfig can not be updated after Build"); throw new InvalidOperationException("TapetiConfig can not be updated after Build");
return config; return config;
} }
/// <inheritdoc /> /// <inheritdoc />
internal class Config : ITapetiConfig internal class Config : ITapetiConfig
{ {
private readonly ConfigFeatures features = new ConfigFeatures(); private readonly ConfigFeatures features = new ConfigFeatures();
private readonly ConfigMiddleware middleware = new ConfigMiddleware(); private readonly ConfigMiddleware middleware = new ConfigMiddleware();
private readonly ConfigBindings bindings = new ConfigBindings(); private readonly ConfigBindings bindings = new ConfigBindings();
public IDependencyResolver DependencyResolver { get; } public IDependencyResolver DependencyResolver { get; }
public ITapetiConfigFeatues Features => features; public ITapetiConfigFeatues Features => features;
public ITapetiConfigMiddleware Middleware => middleware; public ITapetiConfigMiddleware Middleware => middleware;
public ITapetiConfigBindings Bindings => bindings; public ITapetiConfigBindings Bindings => bindings;
public Config(IDependencyResolver dependencyResolver) public Config(IDependencyResolver dependencyResolver)
{ {
DependencyResolver = dependencyResolver; DependencyResolver = dependencyResolver;
} }
public void Lock() public void Lock()
{ {
bindings.Lock(); bindings.Lock();
} }
public void Use(IMessageMiddleware handler) public void Use(IMessageMiddleware handler)
{ {
middleware.Use(handler); middleware.Use(handler);
} }
public void Use(IPublishMiddleware handler) public void Use(IPublishMiddleware handler)
{ {
middleware.Use(handler); middleware.Use(handler);
} }
public void RegisterBinding(IBinding binding) public void RegisterBinding(IBinding binding)
{ {
bindings.Add(binding); bindings.Add(binding);
} }
public void SetPublisherConfirms(bool enabled) public void SetPublisherConfirms(bool enabled)
{ {
features.PublisherConfirms = enabled; features.PublisherConfirms = enabled;
} }
public void SetDeclareDurableQueues(bool enabled) public void SetDeclareDurableQueues(bool enabled)
{ {
features.DeclareDurableQueues = enabled; features.DeclareDurableQueues = enabled;
} }
} }
internal class ConfigFeatures : ITapetiConfigFeatues internal class ConfigFeatures : ITapetiConfigFeatues
{ {
public bool PublisherConfirms { get; internal set; } = true; public bool PublisherConfirms { get; internal set; } = true;
public bool DeclareDurableQueues { get; internal set; } = true; public bool DeclareDurableQueues { get; internal set; } = true;
} }
internal class ConfigMiddleware : ITapetiConfigMiddleware internal class ConfigMiddleware : ITapetiConfigMiddleware
{ {
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>(); private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>(); private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>();
public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware; public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware;
public IReadOnlyList<IPublishMiddleware> Publish => publishMiddleware; public IReadOnlyList<IPublishMiddleware> Publish => publishMiddleware;
public void Use(IMessageMiddleware handler) public void Use(IMessageMiddleware handler)
{ {
messageMiddleware.Add(handler); messageMiddleware.Add(handler);
} }
public void Use(IPublishMiddleware handler) public void Use(IPublishMiddleware handler)
{ {
publishMiddleware.Add(handler); publishMiddleware.Add(handler);
} }
} }
internal class ConfigBindings : List<IBinding>, ITapetiConfigBindings internal class ConfigBindings : List<IBinding>, ITapetiConfigBindings
{ {
private Dictionary<MethodInfo, IControllerMethodBinding> methodLookup; private Dictionary<MethodInfo, IControllerMethodBinding> methodLookup;
public IControllerMethodBinding ForMethod(Delegate method) public IControllerMethodBinding ForMethod(Delegate method)
{ {
return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null; return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null;
} }
public void Lock() public IControllerMethodBinding ForMethod(MethodInfo method)
{ {
methodLookup = this return methodLookup.TryGetValue(method, out var binding) ? binding : null;
.Where(binding => binding is IControllerMethodBinding) }
.Cast<IControllerMethodBinding>()
.ToDictionary(binding => binding.Method, binding => binding);
} public void Lock()
} {
} methodLookup = this
} .Where(binding => binding is IControllerMethodBinding)
.Cast<IControllerMethodBinding>()
.ToDictionary(binding => binding.Method, binding => binding);
}
}
}
}

View File

@ -1,147 +1,150 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Tapeti.Annotations; using Tapeti.Annotations;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Default; using Tapeti.Default;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
namespace Tapeti namespace Tapeti
{ {
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// Thrown when an issue is detected in a controller configuration. /// Thrown when an issue is detected in a controller configuration.
/// </summary> /// </summary>
public class TopologyConfigurationException : Exception public class TopologyConfigurationException : Exception
{ {
/// <inheritdoc /> /// <inheritdoc />
public TopologyConfigurationException(string message) : base(message) { } public TopologyConfigurationException(string message) : base(message) { }
} }
/// <summary> /// <summary>
/// Extension methods for registering message controllers. /// Extension methods for registering message controllers.
/// </summary> /// </summary>
public static class TapetiConfigControllers public static class TapetiConfigControllers
{ {
/// <summary> /// <summary>
/// Registers all public methods in the specified controller class as message handlers. /// Registers all public methods in the specified controller class as message handlers.
/// </summary> /// </summary>
/// <param name="builder"></param> /// <param name="builder"></param>
/// <param name="controller">The controller class to register. The class and/or methods must be annotated with either the DurableQueue or DynamicQueue attribute.</param> /// <param name="controller">The controller class to register. The class and/or methods must be annotated with either the DurableQueue or DynamicQueue attribute.</param>
public static ITapetiConfigBuilder RegisterController(this ITapetiConfigBuilder builder, Type controller) public static ITapetiConfigBuilder RegisterController(this ITapetiConfigBuilder builder, Type controller)
{ {
var builderAccess = (ITapetiConfigBuilderAccess)builder; var builderAccess = (ITapetiConfigBuilderAccess)builder;
if (!controller.IsClass) if (!controller.IsClass)
throw new ArgumentException($"Controller {controller.Name} must be a class"); throw new ArgumentException($"Controller {controller.Name} must be a class");
var controllerQueueInfo = GetQueueInfo(controller); var controllerQueueInfo = GetQueueInfo(controller);
(builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller); (builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller);
var controllerIsObsolete = controller.GetCustomAttribute<ObsoleteAttribute>() != null; var controllerIsObsolete = controller.GetCustomAttribute<ObsoleteAttribute>() != null;
foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance) 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) .Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false)
.Select(m => (MethodInfo)m)) .Select(m => (MethodInfo)m))
{ {
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo; var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
if (methodQueueInfo == null || !methodQueueInfo.IsValid) if (methodQueueInfo == null || !methodQueueInfo.IsValid)
throw new TopologyConfigurationException( throw new TopologyConfigurationException(
$"Method {method.Name} or controller {controller.Name} requires a queue attribute"); $"Method {method.Name} or controller {controller.Name} requires a queue attribute");
var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute<ObsoleteAttribute>() != null; var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute<ObsoleteAttribute>() != null;
var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter) var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter)
{ {
Controller = controller, Controller = controller,
Method = method Method = method
}; };
var allowBinding = false; if (method.GetCustomAttribute<ResponseHandlerAttribute>() != null)
builderAccess.ApplyBindingMiddleware(context, () => { allowBinding = true; }); context.SetBindingTargetMode(BindingTargetMode.Direct);
if (!allowBinding)
continue; var allowBinding = false;
builderAccess.ApplyBindingMiddleware(context, () => { allowBinding = true; });
if (context.MessageClass == null) if (!allowBinding)
throw new TopologyConfigurationException($"Method {method.Name} in controller {controller.Name} does not resolve to a message class"); continue;
var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList(); if (context.MessageClass == null)
if (invalidBindings.Count > 0) throw new TopologyConfigurationException($"Method {method.Name} in controller {controller.Name} does not resolve to a message class");
{
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}"); var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList();
} if (invalidBindings.Count > 0)
{
var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name));
builder.RegisterBinding(new ControllerMethodBinding(builderAccess.DependencyResolver, new ControllerMethodBinding.BindingInfo throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
{ }
ControllerType = controller,
Method = method,
QueueInfo = methodQueueInfo, builder.RegisterBinding(new ControllerMethodBinding(builderAccess.DependencyResolver, new ControllerMethodBinding.BindingInfo
MessageClass = context.MessageClass, {
BindingTargetMode = context.BindingTargetMode, ControllerType = controller,
IsObsolete = methodIsObsolete, Method = method,
ParameterFactories = context.GetParameterHandlers(), QueueInfo = methodQueueInfo,
ResultHandler = context.GetResultHandler(), MessageClass = context.MessageClass,
BindingTargetMode = context.BindingTargetMode,
FilterMiddleware = context.Middleware.Where(m => m is IControllerFilterMiddleware).Cast<IControllerFilterMiddleware>().ToList(), IsObsolete = methodIsObsolete,
MessageMiddleware = context.Middleware.Where(m => m is IControllerMessageMiddleware).Cast<IControllerMessageMiddleware>().ToList(), ParameterFactories = context.GetParameterHandlers(),
CleanupMiddleware = context.Middleware.Where(m => m is IControllerCleanupMiddleware).Cast<IControllerCleanupMiddleware>().ToList() ResultHandler = context.GetResultHandler(),
}));
FilterMiddleware = context.Middleware.Where(m => m is IControllerFilterMiddleware).Cast<IControllerFilterMiddleware>().ToList(),
} MessageMiddleware = context.Middleware.Where(m => m is IControllerMessageMiddleware).Cast<IControllerMessageMiddleware>().ToList(),
CleanupMiddleware = context.Middleware.Where(m => m is IControllerCleanupMiddleware).Cast<IControllerCleanupMiddleware>().ToList()
return builder; }));
} }
return builder;
/// <summary> }
/// Registers all controllers in the specified assembly which are marked with the MessageController attribute.
/// </summary>
/// <param name="builder"></param> /// <summary>
/// <param name="assembly">The assembly to scan for controllers.</param> /// Registers all controllers in the specified assembly which are marked with the MessageController attribute.
public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder, Assembly assembly) /// </summary>
{ /// <param name="builder"></param>
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute)))) /// <param name="assembly">The assembly to scan for controllers.</param>
RegisterController(builder, type); public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder, Assembly assembly)
{
return builder; foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute))))
} RegisterController(builder, type);
return builder;
/// <summary> }
/// Registers all controllers in the entry assembly which are marked with the MessageController attribute.
/// </summary>
/// <param name="builder"></param> /// <summary>
public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder) /// Registers all controllers in the entry assembly which are marked with the MessageController attribute.
{ /// </summary>
return RegisterAllControllers(builder, Assembly.GetEntryAssembly()); /// <param name="builder"></param>
} public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder)
{
return RegisterAllControllers(builder, Assembly.GetEntryAssembly());
private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member) }
{
var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>(); private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member)
{
if (dynamicQueueAttribute != null && durableQueueAttribute != null) var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}"); var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>();
if (dynamicQueueAttribute != null) if (dynamicQueueAttribute != null && durableQueueAttribute != null)
return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix }; throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
return durableQueueAttribute != null if (dynamicQueueAttribute != null)
? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name } return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix };
: null;
} return durableQueueAttribute != null
} ? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name }
} : null;
}
}
}

View File

@ -1,291 +1,291 @@
Flow extension 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. *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). 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 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. 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. See :doc:`indepth` on defining request - response messages.
Enabling Tapeti Flow Enabling Tapeti Flow
-------------------- --------------------
To enable the use of Tapeti Flow, install the Tapeti.Flow NuGet package and call ``WithFlow()`` when setting up your TapetiConfig: 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)) var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container))
.WithFlow() .WithFlow()
.RegisterAllControllers() .RegisterAllControllers()
.Build(); .Build();
Starting a flow 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``. 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. 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. 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<QueryBunniesController>(c => c.StartFlow); await flowStart.Start<QueryBunniesController>(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<IYieldPoint> (for asynchronous methods) or simply IYieldPoint (for synchronous methods). 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<IYieldPoint> (for asynchronous methods) or simply IYieldPoint (for synchronous methods).
:: ::
[MessageController] [MessageController]
[DynamicQueue] [DynamicQueue]
public class QueryBunniesController public class QueryBunniesController
{ {
public DateTime RequestStart { get; set; } public DateTime RequestStart { get; set; }
[Start] [Start]
IYieldPoint StartFlow() public IYieldPoint StartFlow()
{ {
RequestStart = DateTime.UtcNow(); 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. 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<QueryBunniesController>(c => c.StartFlow, "pink"); await flowStart.Start<QueryBunniesController>(c => c.StartFlow, "pink");
[MessageController] [MessageController]
[DynamicQueue] [DynamicQueue]
public class QueryBunniesController public class QueryBunniesController
{ {
public DateTime RequestStart { get; set; } public DateTime RequestStart { get; set; }
[Start] [Start]
IYieldPoint StartFlow(string colorFilter) public IYieldPoint StartFlow(string colorFilter)
{ {
RequestStart = DateTime.UtcNow(); 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. .. 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 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. 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. 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<IYieldPoint> itself. 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<IYieldPoint> itself.
If the response handler is not asynchronous, use ``YieldWithRequestSync`` instead, as used in the example below: If the response handler is not asynchronous, use ``YieldWithRequestSync`` instead, as used in the example below:
:: ::
[MessageController] [MessageController]
[DynamicQueue] [DynamicQueue]
public class QueryBunniesController public class QueryBunniesController
{ {
private IFlowProvider flowProvider; private IFlowProvider flowProvider;
public DateTime RequestStart { get; set; } public DateTime RequestStart { get; set; }
public QueryBunniesController(IFlowProvider flowProvider) public QueryBunniesController(IFlowProvider flowProvider)
{ {
this.flowProvider = flowProvider; this.flowProvider = flowProvider;
} }
[Start] [Start]
IYieldPoint StartFlow(string colorFilter) public IYieldPoint StartFlow(string colorFilter)
{ {
RequestStart = DateTime.UtcNow(); RequestStart = DateTime.UtcNow();
var request = new BunnyCountRequestMessage var request = new BunnyCountRequestMessage
{ {
ColorFilter = colorFilter ColorFilter = colorFilter
}; };
return flowProvider.YieldWithRequestSync<BunnyCountRequestMessage, BunnyCountResponseMessage> return flowProvider.YieldWithRequestSync<BunnyCountRequestMessage, BunnyCountResponseMessage>
(request, HandleBunnyCountResponse); (request, HandleBunnyCountResponse);
} }
[Continuation] [Continuation]
public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message)
{ {
// Handle the response. The original RequestStart is available here as well. // Handle the response. The original RequestStart is available here as well.
} }
} }
You can once again return a ``YieldWithRequest``, or end it. You can once again return a ``YieldWithRequest``, or end it.
Ending a flow Ending a flow
------------- -------------
To end the flow and dispose of any stored state, return an end yieldpoint: To end the flow and dispose of any stored state, return an end yieldpoint:
:: ::
[Continuation] [Continuation]
public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message) public IYieldPoint HandleBunnyCountResponse(BunnyCountResponseMessage message)
{ {
// Handle the response. // Handle the response.
return flowProvider.End(); return flowProvider.End();
} }
Flows started by a (request) message 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: 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] [MessageController]
[DurableQueue("hutch")] [DurableQueue("hutch")]
public class HutchController public class HutchController
{ {
private IBunnyRepository repository; private IBunnyRepository repository;
private IFlowProvider flowProvider; private IFlowProvider flowProvider;
public string ColorFilter { get; set; } public string ColorFilter { get; set; }
public HutchController(IBunnyRepository repository, IFlowProvider flowProvider) public HutchController(IBunnyRepository repository, IFlowProvider flowProvider)
{ {
this.repository = repository; this.repository = repository;
this.flowProvider = flowProvider; this.flowProvider = flowProvider;
} }
public IYieldPoint HandleCountRequest(BunnyCountRequestMessage message) public IYieldPoint HandleCountRequest(BunnyCountRequestMessage message)
{ {
ColorFilter = message.ColorFilter; ColorFilter = message.ColorFilter;
return flowProvider.YieldWithRequestSync<CheckAccessRequestMessage, CheckAccessResponseMessage> return flowProvider.YieldWithRequestSync<CheckAccessRequestMessage, CheckAccessResponseMessage>
( (
new CheckAccessRequestMessage new CheckAccessRequestMessage
{ {
Username = "hutch" Username = "hutch"
}, },
HandleCheckAccessResponseMessage HandleCheckAccessResponseMessage
); );
} }
[Continuation] [Continuation]
public IYieldPoint HandleCheckAccessResponseMessage(CheckAccessResponseMessage message) public IYieldPoint HandleCheckAccessResponseMessage(CheckAccessResponseMessage message)
{ {
// We must provide a response to our original BunnyCountRequestMessage // We must provide a response to our original BunnyCountRequestMessage
return flowProvider.EndWithResponse(new BunnyCountResponseMessage return flowProvider.EndWithResponse(new BunnyCountResponseMessage
{ {
Count = message.HasAccess ? await repository.Count(ColorFilter) : 0 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. .. 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 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. 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: An example:
:: ::
public IYieldPoint HandleBirthdayMessage(RabbitBirthdayMessage message) public IYieldPoint HandleBirthdayMessage(RabbitBirthdayMessage message)
{ {
var sendCardRequest = new SendCardRequestMessage var sendCardRequest = new SendCardRequestMessage
{ {
RabbitID = message.RabbitID, RabbitID = message.RabbitID,
Age = message.Age, Age = message.Age,
Style = CardStyles.Funny Style = CardStyles.Funny
}; };
var doctorAppointmentMessage = new DoctorAppointmentRequestMessage var doctorAppointmentMessage = new DoctorAppointmentRequestMessage
{ {
RabbitID = message.RabbitID, RabbitID = message.RabbitID,
Reason = "Yearly checkup" Reason = "Yearly checkup"
}; };
return flowProvider.YieldWithParallelRequest() return flowProvider.YieldWithParallelRequest()
.AddRequestSync<SendCardRequestMessage, SendCardResponseMessage>( .AddRequestSync<SendCardRequestMessage, SendCardResponseMessage>(
sendCardRequest, HandleCardResponse) sendCardRequest, HandleCardResponse)
.AddRequestSync<DoctorAppointmentRequestMessage, DoctorAppointmentResponseMessage>( .AddRequestSync<DoctorAppointmentRequestMessage, DoctorAppointmentResponseMessage>(
doctorAppointmentMessage, HandleDoctorAppointmentResponse) doctorAppointmentMessage, HandleDoctorAppointmentResponse)
.YieldSync(ContinueAfterResponses); .YieldSync(ContinueAfterResponses);
} }
[Continuation] [Continuation]
public void HandleCardResponse(SendCardResponseMessage message) public void HandleCardResponse(SendCardResponseMessage message)
{ {
// Handle card response. For example, store the result in a public field // Handle card response. For example, store the result in a public field
} }
[Continuation] [Continuation]
public void HandleDoctorAppointmentResponse(DoctorAppointmentResponseMessage message) public void HandleDoctorAppointmentResponse(DoctorAppointmentResponseMessage message)
{ {
// Handle appointment response. Note that the order of the responses is not guaranteed, // 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 // but the handlers will never run at the same time, so it is safe to access
// and manipulate the public fields of the controller. // and manipulate the public fields of the controller.
} }
private IYieldPoint ContinueAfterResponses() private IYieldPoint ContinueAfterResponses()
{ {
// Perform further operations on the results stored in the public fields // Perform further operations on the results stored in the public fields
// This flow did not start with a request message, so end it normally // This flow did not start with a request message, so end it normally
return flowProvider.End(); return flowProvider.End();
} }
A few things to note: 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 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. #) The converge method must be private, as it is not a valid message handler in itself.
#) You must add at least one request. #) 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. 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 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()``. 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)) var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container))
.WithFlow(new MyFlowRepository()) .WithFlow(new MyFlowRepository())
.RegisterAllControllers() .RegisterAllControllers()
.Build(); .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: 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 create table Flow
( (
FlowID uniqueidentifier not null, FlowID uniqueidentifier not null,
CreationTime datetime2(3) not null, CreationTime datetime2(3) not null,
StateJson nvarchar(max) null, StateJson nvarchar(max) null,
constraint PK_Flow primary key clustered(FlowID) 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``: 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)) var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container))
.WithFlowSqlRepository("Server=localhost;Database=TapetiTest;Integrated Security=true") .WithFlowSqlRepository("Server=localhost;Database=TapetiTest;Integrated Security=true")
.WithFlow() .WithFlow()
.RegisterAllControllers() .RegisterAllControllers()
.Build(); .Build();