[ci skip] Major refactoring for 2.0
- Compiles, but that's about it. Plenty of ToDo's left before it will run. Beware, ye who enter here. - Cleanup of the internals, with the aim to keep the interface to application code compatible - Added the ability to declare durable queues on startup and update the bindings - Possibly fixed an issue with publish timeouts being logged after a reconnect
This commit is contained in:
parent
70f394f7fe
commit
f8fca5879c
@ -5,4 +5,8 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.5.0" />
|
<PackageReference Include="System.Data.SqlClient" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -8,15 +8,15 @@ using Tapeti.Helpers;
|
|||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
namespace Tapeti.Flow.Default
|
||||||
{
|
{
|
||||||
internal class FlowBindingMiddleware : IBindingMiddleware
|
internal class FlowBindingMiddleware : IControllerBindingMiddleware
|
||||||
{
|
{
|
||||||
public void Handle(IBindingContext context, Action next)
|
public void Handle(IControllerBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
if (context.Method.GetCustomAttribute<StartAttribute>() != null)
|
if (context.Method.GetCustomAttribute<StartAttribute>() != null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (context.Method.GetCustomAttribute<ContinuationAttribute>() != null)
|
if (context.Method.GetCustomAttribute<ContinuationAttribute>() != null)
|
||||||
context.QueueBindingMode = QueueBindingMode.DirectToQueue;
|
context.SetBindingTargetMode(BindingTargetMode.Direct);
|
||||||
|
|
||||||
RegisterYieldPointResult(context);
|
RegisterYieldPointResult(context);
|
||||||
RegisterContinuationFilter(context);
|
RegisterContinuationFilter(context);
|
||||||
@ -27,14 +27,13 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void RegisterContinuationFilter(IBindingContext context)
|
private static void RegisterContinuationFilter(IControllerBindingContext context)
|
||||||
{
|
{
|
||||||
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
|
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
|
||||||
if (continuationAttribute == null)
|
if (continuationAttribute == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
context.Use(new FlowMessageFilterMiddleware());
|
context.Use(new FlowMiddleware());
|
||||||
context.Use(new FlowMessageMiddleware());
|
|
||||||
|
|
||||||
if (context.Result.HasHandler)
|
if (context.Result.HasHandler)
|
||||||
return;
|
return;
|
||||||
@ -58,7 +57,7 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void RegisterYieldPointResult(IBindingContext context)
|
private static void RegisterYieldPointResult(IControllerBindingContext context)
|
||||||
{
|
{
|
||||||
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out var isTaskOf))
|
if (!context.Result.Info.ParameterType.IsTypeOrTaskOf(typeof(IYieldPoint), out var isTaskOf))
|
||||||
return;
|
return;
|
||||||
@ -77,16 +76,16 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Task HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
|
private static Task HandleYieldPoint(IControllerMessageContext context, IYieldPoint yieldPoint)
|
||||||
{
|
{
|
||||||
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
return flowHandler.Execute(context, yieldPoint);
|
return flowHandler.Execute(context, yieldPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Task HandleParallelResponse(IMessageContext context)
|
private static Task HandleParallelResponse(IControllerMessageContext context)
|
||||||
{
|
{
|
||||||
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
return flowHandler.Execute(context, new DelegateYieldPoint(async flowContext =>
|
return flowHandler.Execute(context, new DelegateYieldPoint(async flowContext =>
|
||||||
{
|
{
|
||||||
await flowContext.Store();
|
await flowContext.Store();
|
||||||
@ -94,7 +93,7 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void ValidateRequestResponse(IBindingContext context)
|
private static void ValidateRequestResponse(IControllerBindingContext context)
|
||||||
{
|
{
|
||||||
var request = context.MessageClass?.GetCustomAttribute<RequestAttribute>();
|
var request = context.MessageClass?.GetCustomAttribute<RequestAttribute>();
|
||||||
if (request?.Response == null)
|
if (request?.Response == null)
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
|
||||||
{
|
|
||||||
public class FlowCleanupMiddleware : ICleanupMiddleware
|
|
||||||
{
|
|
||||||
public async Task Handle(IMessageContext context, HandlingResult handlingResult)
|
|
||||||
{
|
|
||||||
if (!context.Items.TryGetValue(ContextItems.FlowContext, out var flowContextObj))
|
|
||||||
return;
|
|
||||||
var flowContext = (FlowContext)flowContextObj;
|
|
||||||
|
|
||||||
if (flowContext?.FlowStateLock != null)
|
|
||||||
{
|
|
||||||
if (handlingResult.ConsumeResponse == ConsumeResponse.Nack
|
|
||||||
|| handlingResult.MessageAction == MessageAction.ErrorLog)
|
|
||||||
{
|
|
||||||
await flowContext.FlowStateLock.DeleteFlowState();
|
|
||||||
}
|
|
||||||
flowContext.FlowStateLock.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
internal class FlowContext : IDisposable
|
internal class FlowContext : IDisposable
|
||||||
{
|
{
|
||||||
public IMessageContext MessageContext { get; set; }
|
public IControllerMessageContext MessageContext { get; set; }
|
||||||
public IFlowStateLock FlowStateLock { get; set; }
|
public IFlowStateLock FlowStateLock { get; set; }
|
||||||
public FlowState FlowState { get; set; }
|
public FlowState FlowState { get; set; }
|
||||||
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Tapeti.Config;
|
|
||||||
using Tapeti.Flow.FlowHelpers;
|
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
|
||||||
{
|
|
||||||
public class FlowMessageFilterMiddleware : IMessageFilterMiddleware
|
|
||||||
{
|
|
||||||
public async Task Handle(IMessageContext context, Func<Task> next)
|
|
||||||
{
|
|
||||||
var flowContext = await GetFlowContext(context);
|
|
||||||
if (flowContext?.ContinuationMetadata == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(context.Binding.Method))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static async Task<FlowContext> GetFlowContext(IMessageContext context)
|
|
||||||
{
|
|
||||||
if (context.Items.ContainsKey(ContextItems.FlowContext))
|
|
||||||
return (FlowContext)context.Items[ContextItems.FlowContext];
|
|
||||||
|
|
||||||
if (context.Properties.CorrelationId == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(context.Properties.CorrelationId, out var continuationID))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var flowStore = context.DependencyResolver.Resolve<IFlowStore>();
|
|
||||||
|
|
||||||
var flowID = await flowStore.FindFlowID(continuationID);
|
|
||||||
if (!flowID.HasValue)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var flowStateLock = await flowStore.LockFlowState(flowID.Value);
|
|
||||||
|
|
||||||
var flowState = await flowStateLock.GetFlowState();
|
|
||||||
if (flowState == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var flowContext = new FlowContext
|
|
||||||
{
|
|
||||||
MessageContext = context,
|
|
||||||
|
|
||||||
FlowStateLock = flowStateLock,
|
|
||||||
FlowState = flowState,
|
|
||||||
|
|
||||||
ContinuationID = continuationID,
|
|
||||||
ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out var continuation) ? continuation : null
|
|
||||||
};
|
|
||||||
|
|
||||||
// IDisposable items in the IMessageContext are automatically disposed
|
|
||||||
context.Items.Add(ContextItems.FlowContext, flowContext);
|
|
||||||
return flowContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
|
||||||
{
|
|
||||||
public class FlowMessageMiddleware : IMessageMiddleware
|
|
||||||
{
|
|
||||||
public async Task Handle(IMessageContext context, Func<Task> next)
|
|
||||||
{
|
|
||||||
var flowContext = (FlowContext)context.Items[ContextItems.FlowContext];
|
|
||||||
if (flowContext != null)
|
|
||||||
{
|
|
||||||
Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, context.Controller);
|
|
||||||
|
|
||||||
// Remove Continuation now because the IYieldPoint result handler will store the new state
|
|
||||||
flowContext.FlowState.Continuations.Remove(flowContext.ContinuationID);
|
|
||||||
var converge = flowContext.FlowState.Continuations.Count == 0 &&
|
|
||||||
flowContext.ContinuationMetadata.ConvergeMethodName != null;
|
|
||||||
|
|
||||||
await next();
|
|
||||||
|
|
||||||
if (converge)
|
|
||||||
await CallConvergeMethod(context,
|
|
||||||
flowContext.ContinuationMetadata.ConvergeMethodName,
|
|
||||||
flowContext.ContinuationMetadata.ConvergeMethodSync);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static async Task CallConvergeMethod(IMessageContext context, string methodName, bool sync)
|
|
||||||
{
|
|
||||||
IYieldPoint yieldPoint;
|
|
||||||
|
|
||||||
var method = context.Controller.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
|
|
||||||
if (method == null)
|
|
||||||
throw new ArgumentException($"Unknown converge method in controller {context.Controller.GetType().Name}: {methodName}");
|
|
||||||
|
|
||||||
if (sync)
|
|
||||||
yieldPoint = (IYieldPoint)method.Invoke(context.Controller, new object[] {});
|
|
||||||
else
|
|
||||||
yieldPoint = await (Task<IYieldPoint>)method.Invoke(context.Controller, new object[] { });
|
|
||||||
|
|
||||||
if (yieldPoint == null)
|
|
||||||
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for converge method {methodName}");
|
|
||||||
|
|
||||||
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
|
|
||||||
await flowHandler.Execute(context, yieldPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
128
Tapeti.Flow/Default/FlowMiddleware.cs
Normal file
128
Tapeti.Flow/Default/FlowMiddleware.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Flow.FlowHelpers;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class FlowMiddleware : IControllerFilterMiddleware, IControllerMessageMiddleware, IControllerCleanupMiddleware
|
||||||
|
{
|
||||||
|
public async Task Filter(IControllerMessageContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
var flowContext = await CreateFlowContext(context);
|
||||||
|
if (flowContext?.ContinuationMetadata == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(context.Binding.Method))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Handle(IControllerMessageContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||||
|
{
|
||||||
|
Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, context.Controller);
|
||||||
|
|
||||||
|
// Remove Continuation now because the IYieldPoint result handler will store the new state
|
||||||
|
flowContext.FlowState.Continuations.Remove(flowContext.ContinuationID);
|
||||||
|
var converge = flowContext.FlowState.Continuations.Count == 0 &&
|
||||||
|
flowContext.ContinuationMetadata.ConvergeMethodName != null;
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
if (converge)
|
||||||
|
await CallConvergeMethod(context,
|
||||||
|
flowContext.ContinuationMetadata.ConvergeMethodName,
|
||||||
|
flowContext.ContinuationMetadata.ConvergeMethodSync);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Cleanup(IControllerMessageContext context, HandlingResult handlingResult, Func<Task> next)
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
|
||||||
|
if (!context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (flowContext?.FlowStateLock != null)
|
||||||
|
{
|
||||||
|
if (handlingResult.ConsumeResponse == ConsumeResponse.Nack
|
||||||
|
|| handlingResult.MessageAction == MessageAction.ErrorLog)
|
||||||
|
{
|
||||||
|
await flowContext.FlowStateLock.DeleteFlowState();
|
||||||
|
}
|
||||||
|
flowContext.FlowStateLock.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static async Task<FlowContext> CreateFlowContext(IControllerMessageContext context)
|
||||||
|
{
|
||||||
|
if (context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||||
|
return flowContext;
|
||||||
|
|
||||||
|
|
||||||
|
if (context.Properties.CorrelationId == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!Guid.TryParse(context.Properties.CorrelationId, out var continuationID))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flowStore = context.Config.DependencyResolver.Resolve<IFlowStore>();
|
||||||
|
|
||||||
|
var flowID = await flowStore.FindFlowID(continuationID);
|
||||||
|
if (!flowID.HasValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flowStateLock = await flowStore.LockFlowState(flowID.Value);
|
||||||
|
|
||||||
|
var flowState = await flowStateLock.GetFlowState();
|
||||||
|
if (flowState == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
flowContext = new FlowContext
|
||||||
|
{
|
||||||
|
MessageContext = context,
|
||||||
|
|
||||||
|
FlowStateLock = flowStateLock,
|
||||||
|
FlowState = flowState,
|
||||||
|
|
||||||
|
ContinuationID = continuationID,
|
||||||
|
ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out var continuation) ? continuation : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// IDisposable items in the IMessageContext are automatically disposed
|
||||||
|
context.Store(ContextItems.FlowContext, flowContext);
|
||||||
|
return flowContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static async Task CallConvergeMethod(IControllerMessageContext context, string methodName, bool sync)
|
||||||
|
{
|
||||||
|
IYieldPoint yieldPoint;
|
||||||
|
|
||||||
|
var method = context.Controller.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (method == null)
|
||||||
|
throw new ArgumentException($"Unknown converge method in controller {context.Controller.GetType().Name}: {methodName}");
|
||||||
|
|
||||||
|
if (sync)
|
||||||
|
yieldPoint = (IYieldPoint)method.Invoke(context.Controller, new object[] {});
|
||||||
|
else
|
||||||
|
yieldPoint = await (Task<IYieldPoint>)method.Invoke(context.Controller, new object[] { });
|
||||||
|
|
||||||
|
if (yieldPoint == null)
|
||||||
|
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for converge method {methodName}");
|
||||||
|
|
||||||
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
|
await flowHandler.Execute(context, yieldPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,9 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client.Framing;
|
|
||||||
using Tapeti.Annotations;
|
using Tapeti.Annotations;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Default;
|
||||||
using Tapeti.Flow.Annotations;
|
using Tapeti.Flow.Annotations;
|
||||||
using Tapeti.Flow.FlowHelpers;
|
using Tapeti.Flow.FlowHelpers;
|
||||||
|
|
||||||
@ -14,11 +14,11 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
public class FlowProvider : IFlowProvider, IFlowHandler
|
public class FlowProvider : IFlowProvider, IFlowHandler
|
||||||
{
|
{
|
||||||
private readonly IConfig config;
|
private readonly ITapetiConfig config;
|
||||||
private readonly IInternalPublisher publisher;
|
private readonly IInternalPublisher publisher;
|
||||||
|
|
||||||
|
|
||||||
public FlowProvider(IConfig config, IPublisher publisher)
|
public FlowProvider(ITapetiConfig config, IPublisher publisher)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.publisher = (IInternalPublisher)publisher;
|
this.publisher = (IInternalPublisher)publisher;
|
||||||
@ -72,7 +72,7 @@ namespace Tapeti.Flow.Default
|
|||||||
ConvergeMethodSync = convergeMethodTaskSync
|
ConvergeMethodSync = convergeMethodTaskSync
|
||||||
});
|
});
|
||||||
|
|
||||||
var properties = new BasicProperties
|
var properties = new MessageProperties
|
||||||
{
|
{
|
||||||
CorrelationId = continuationID.ToString(),
|
CorrelationId = continuationID.ToString(),
|
||||||
ReplyTo = responseHandlerInfo.ReplyToQueue
|
ReplyTo = responseHandlerInfo.ReplyToQueue
|
||||||
@ -96,12 +96,10 @@ namespace Tapeti.Flow.Default
|
|||||||
if (message.GetType().FullName != reply.ResponseTypeName)
|
if (message.GetType().FullName != reply.ResponseTypeName)
|
||||||
throw new YieldPointException($"Flow must end with a response message of type {reply.ResponseTypeName}, {message.GetType().FullName} was returned instead");
|
throw new YieldPointException($"Flow must end with a response message of type {reply.ResponseTypeName}, {message.GetType().FullName} was returned instead");
|
||||||
|
|
||||||
var properties = new BasicProperties();
|
var properties = new MessageProperties
|
||||||
|
{
|
||||||
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
CorrelationId = reply.CorrelationId
|
||||||
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
};
|
||||||
if (reply.CorrelationId != null)
|
|
||||||
properties.CorrelationId = reply.CorrelationId;
|
|
||||||
|
|
||||||
// TODO disallow if replyto is not specified?
|
// TODO disallow if replyto is not specified?
|
||||||
if (reply.ReplyTo != null)
|
if (reply.ReplyTo != null)
|
||||||
@ -122,9 +120,9 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static ResponseHandlerInfo GetResponseHandlerInfo(IConfig config, object request, Delegate responseHandler)
|
private static ResponseHandlerInfo GetResponseHandlerInfo(ITapetiConfig config, object request, Delegate responseHandler)
|
||||||
{
|
{
|
||||||
var binding = config.GetBinding(responseHandler);
|
var binding = config.Bindings.ForMethod(responseHandler);
|
||||||
if (binding == null)
|
if (binding == null)
|
||||||
throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler));
|
throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler));
|
||||||
|
|
||||||
@ -158,13 +156,13 @@ namespace Tapeti.Flow.Default
|
|||||||
CorrelationId = context.Properties.CorrelationId,
|
CorrelationId = context.Properties.CorrelationId,
|
||||||
ReplyTo = context.Properties.ReplyTo,
|
ReplyTo = context.Properties.ReplyTo,
|
||||||
ResponseTypeName = requestAttribute.Response.FullName,
|
ResponseTypeName = requestAttribute.Response.FullName,
|
||||||
Mandatory = context.Properties.Persistent
|
Mandatory = context.Properties.Persistent.GetValueOrDefault(true)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateNewFlowState(FlowContext flowContext)
|
private static async Task CreateNewFlowState(FlowContext flowContext)
|
||||||
{
|
{
|
||||||
var flowStore = flowContext.MessageContext.DependencyResolver.Resolve<IFlowStore>();
|
var flowStore = flowContext.MessageContext.Config.DependencyResolver.Resolve<IFlowStore>();
|
||||||
|
|
||||||
var flowID = Guid.NewGuid();
|
var flowID = Guid.NewGuid();
|
||||||
flowContext.FlowStateLock = await flowStore.LockFlowState(flowID);
|
flowContext.FlowStateLock = await flowStore.LockFlowState(flowID);
|
||||||
@ -181,25 +179,20 @@ namespace Tapeti.Flow.Default
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IMessageContext context, IYieldPoint yieldPoint)
|
public async Task Execute(IControllerMessageContext context, IYieldPoint yieldPoint)
|
||||||
{
|
{
|
||||||
if (!(yieldPoint is DelegateYieldPoint executableYieldPoint))
|
if (!(yieldPoint is DelegateYieldPoint executableYieldPoint))
|
||||||
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Binding.Method.Name}");
|
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Binding.Method.Name}");
|
||||||
|
|
||||||
FlowContext flowContext;
|
if (!context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||||
|
|
||||||
if (!context.Items.TryGetValue(ContextItems.FlowContext, out var flowContextItem))
|
|
||||||
{
|
{
|
||||||
flowContext = new FlowContext
|
flowContext = new FlowContext
|
||||||
{
|
{
|
||||||
MessageContext = context
|
MessageContext = context
|
||||||
};
|
};
|
||||||
|
|
||||||
context.Items.Add(ContextItems.FlowContext, flowContext);
|
context.Store(ContextItems.FlowContext, flowContext);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
flowContext = (FlowContext)flowContextItem;
|
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -234,12 +227,12 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private readonly IConfig config;
|
private readonly ITapetiConfig config;
|
||||||
private readonly SendRequestFunc sendRequest;
|
private readonly SendRequestFunc sendRequest;
|
||||||
private readonly List<RequestInfo> requests = new List<RequestInfo>();
|
private readonly List<RequestInfo> requests = new List<RequestInfo>();
|
||||||
|
|
||||||
|
|
||||||
public ParallelRequestBuilder(IConfig config, SendRequestFunc sendRequest)
|
public ParallelRequestBuilder(ITapetiConfig config, SendRequestFunc sendRequest)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.sendRequest = sendRequest;
|
this.sendRequest = sendRequest;
|
||||||
|
@ -9,11 +9,11 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
public class FlowStarter : IFlowStarter
|
public class FlowStarter : IFlowStarter
|
||||||
{
|
{
|
||||||
private readonly IConfig config;
|
private readonly ITapetiConfig config;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
|
|
||||||
|
|
||||||
public FlowStarter(IConfig config, ILogger logger)
|
public FlowStarter(ITapetiConfig config, ILogger logger)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
@ -47,9 +47,9 @@ namespace Tapeti.Flow.Default
|
|||||||
var controller = config.DependencyResolver.Resolve<TController>();
|
var controller = config.DependencyResolver.Resolve<TController>();
|
||||||
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
|
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
|
||||||
|
|
||||||
var context = new MessageContext
|
var context = new ControllerMessageContext
|
||||||
{
|
{
|
||||||
DependencyResolver = config.DependencyResolver,
|
Config = config,
|
||||||
Controller = controller
|
Controller = controller
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,6 +72,7 @@ namespace Tapeti.Flow.Default
|
|||||||
|
|
||||||
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
foreach (var handler in config.CleanupMiddleware)
|
foreach (var handler in config.CleanupMiddleware)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -83,6 +84,7 @@ namespace Tapeti.Flow.Default
|
|||||||
logger.HandlerException(eCleanup);
|
logger.HandlerException(eCleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ namespace Tapeti.Flow
|
|||||||
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
|
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
yield return new FlowBindingMiddleware();
|
yield return new FlowBindingMiddleware();
|
||||||
yield return new FlowCleanupMiddleware();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ namespace Tapeti.Flow
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFlowHandler
|
public interface IFlowHandler
|
||||||
{
|
{
|
||||||
Task Execute(IMessageContext context, IYieldPoint yieldPoint);
|
Task Execute(IControllerMessageContext context, IYieldPoint yieldPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IFlowParallelRequestBuilder
|
public interface IFlowParallelRequestBuilder
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tapeti.Annotations\Tapeti.Annotations.csproj" />
|
<ProjectReference Include="..\Tapeti.Annotations\Tapeti.Annotations.csproj" />
|
||||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SimpleInjector" Version="4.3.0" />
|
<PackageReference Include="SimpleInjector" Version="4.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Tapeti.Default;
|
using Tapeti.Default;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Tapeti.Tests
|
namespace Tapeti.Tests.Default
|
||||||
{
|
{
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
public class TypeNameRoutingKeyStrategyTests
|
public class TypeNameRoutingKeyStrategyTests
|
@ -1,9 +1,8 @@
|
|||||||
using Tapeti.Helpers;
|
using Tapeti.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Tapeti.Tests
|
namespace Tapeti.Tests.Helpers
|
||||||
{
|
{
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
public class ConnectionStringParserTest
|
public class ConnectionStringParserTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
@ -4,6 +4,10 @@
|
|||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||||
<PackageReference Include="xunit" Version="2.3.1" />
|
<PackageReference Include="xunit" Version="2.3.1" />
|
||||||
@ -14,4 +18,8 @@
|
|||||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Core\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.Tests
|
|
||||||
{
|
|
||||||
public class TransientFilterMiddleware : IMessageFilterMiddleware
|
|
||||||
{
|
|
||||||
public Task Handle(IMessageContext context, Func<Task> next)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TapetiConfig extension to register Tapeti.Transient
|
||||||
|
/// </summary>
|
||||||
public static class ConfigExtensions
|
public static class ConfigExtensions
|
||||||
{
|
{
|
||||||
public static TapetiConfig WithTransient(this TapetiConfig config, TimeSpan defaultTimeout, string dynamicQueuePrefix = "transient")
|
/// <summary>
|
||||||
|
/// Registers the transient publisher and required middleware
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config"></param>
|
||||||
|
/// <param name="defaultTimeout"></param>
|
||||||
|
/// <param name="dynamicQueuePrefix"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static ITapetiConfigBuilder WithTransient(this ITapetiConfigBuilder config, TimeSpan defaultTimeout, string dynamicQueuePrefix = "transient")
|
||||||
{
|
{
|
||||||
config.Use(new TransientMiddleware(defaultTimeout, dynamicQueuePrefix));
|
config.Use(new TransientExtension(defaultTimeout, dynamicQueuePrefix));
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a publisher which can send request messages, and await the response on a dynamic queue.
|
||||||
|
/// </summary>
|
||||||
public interface ITransientPublisher
|
public interface ITransientPublisher
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a request and waits for the response with the timeout specified in the WithTransient config call.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <typeparam name="TRequest"></typeparam>
|
||||||
|
/// <typeparam name="TResponse"></typeparam>
|
||||||
Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request);
|
Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -4,29 +4,38 @@ using Tapeti.Config;
|
|||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
public class TransientMiddleware : ITapetiExtension, ITapetiExtentionBinding
|
/// <inheritdoc cref="ITapetiExtension" />
|
||||||
|
public class TransientExtension : ITapetiExtensionBinding
|
||||||
{
|
{
|
||||||
private string dynamicQueuePrefix;
|
private readonly string dynamicQueuePrefix;
|
||||||
private readonly TransientRouter router;
|
private readonly TransientRouter router;
|
||||||
|
|
||||||
public TransientMiddleware(TimeSpan defaultTimeout, string dynamicQueuePrefix)
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TransientExtension(TimeSpan defaultTimeout, string dynamicQueuePrefix)
|
||||||
{
|
{
|
||||||
this.dynamicQueuePrefix = dynamicQueuePrefix;
|
this.dynamicQueuePrefix = dynamicQueuePrefix;
|
||||||
this.router = new TransientRouter(defaultTimeout);
|
router = new TransientRouter(defaultTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void RegisterDefaults(IDependencyContainer container)
|
public void RegisterDefaults(IDependencyContainer container)
|
||||||
{
|
{
|
||||||
container.RegisterDefaultSingleton(router);
|
container.RegisterDefaultSingleton(router);
|
||||||
container.RegisterDefault<ITransientPublisher, TransientPublisher>();
|
container.RegisterDefault<ITransientPublisher, TransientPublisher>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
|
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
return new object[0];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ICustomBinding> GetBindings(IDependencyResolver dependencyResolver)
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IBinding> GetBindings(IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
yield return new TransientGenericBinding(router, dynamicQueuePrefix);
|
yield return new TransientGenericBinding(router, dynamicQueuePrefix);
|
||||||
}
|
}
|
@ -1,52 +1,51 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
public class TransientGenericBinding : ICustomBinding
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a binding for transient request response messages.
|
||||||
|
/// Register this binding using the WithTransient config extension method.
|
||||||
|
/// </summary>
|
||||||
|
public class TransientGenericBinding : IBinding
|
||||||
{
|
{
|
||||||
private readonly TransientRouter router;
|
private readonly TransientRouter router;
|
||||||
|
private readonly string dynamicQueuePrefix;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string QueueName { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TransientGenericBinding(TransientRouter router, string dynamicQueuePrefix)
|
public TransientGenericBinding(TransientRouter router, string dynamicQueuePrefix)
|
||||||
{
|
{
|
||||||
this.router = router;
|
this.router = router;
|
||||||
DynamicQueuePrefix = dynamicQueuePrefix;
|
this.dynamicQueuePrefix = dynamicQueuePrefix;
|
||||||
Method = typeof(TransientRouter).GetMethod("GenericHandleResponse");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Type Controller => typeof(TransientRouter);
|
|
||||||
|
|
||||||
public MethodInfo Method { get; }
|
/// <inheritdoc />
|
||||||
|
public async Task Apply(IBindingTarget target)
|
||||||
|
{
|
||||||
|
QueueName = await target.BindDirectDynamic(dynamicQueuePrefix);
|
||||||
|
router.TransientResponseQueueName = QueueName;
|
||||||
|
}
|
||||||
|
|
||||||
public QueueBindingMode QueueBindingMode => QueueBindingMode.DirectToQueue;
|
|
||||||
|
|
||||||
public string StaticQueueName => null;
|
|
||||||
|
|
||||||
public string DynamicQueuePrefix { get; }
|
|
||||||
|
|
||||||
public Type MessageClass => null;
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool Accept(Type messageClass)
|
public bool Accept(Type messageClass)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Accept(IMessageContext context, object message)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Invoke(IMessageContext context, object message)
|
/// <inheritdoc />
|
||||||
|
public Task Invoke(IMessageContext context)
|
||||||
{
|
{
|
||||||
router.GenericHandleResponse(message, context);
|
router.HandleMessage(context);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetQueueName(string queueName)
|
|
||||||
{
|
|
||||||
router.TransientResponseQueueName = queueName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +1,26 @@
|
|||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of ITransientPublisher
|
||||||
|
/// </summary>
|
||||||
public class TransientPublisher : ITransientPublisher
|
public class TransientPublisher : ITransientPublisher
|
||||||
{
|
{
|
||||||
private readonly TransientRouter router;
|
private readonly TransientRouter router;
|
||||||
private readonly IPublisher publisher;
|
private readonly IPublisher publisher;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TransientPublisher(TransientRouter router, IPublisher publisher)
|
public TransientPublisher(TransientRouter router, IPublisher publisher)
|
||||||
{
|
{
|
||||||
this.router = router;
|
this.router = router;
|
||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request)
|
public async Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request)
|
||||||
{
|
{
|
||||||
return (TResponse)(await router.RequestResponse(publisher, request));
|
return (TResponse)(await router.RequestResponse(publisher, request));
|
||||||
|
@ -2,26 +2,37 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using RabbitMQ.Client.Framing;
|
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Default;
|
||||||
|
|
||||||
namespace Tapeti.Transient
|
namespace Tapeti.Transient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages active requests and responses. For internal use.
|
||||||
|
/// </summary>
|
||||||
public class TransientRouter
|
public class TransientRouter
|
||||||
{
|
{
|
||||||
private readonly int defaultTimeoutMs;
|
private readonly int defaultTimeoutMs;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>();
|
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> map = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The generated name of the dynamic queue to which responses should be sent.
|
||||||
|
/// </summary>
|
||||||
public string TransientResponseQueueName { get; set; }
|
public string TransientResponseQueueName { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TransientRouter(TimeSpan defaultTimeout)
|
public TransientRouter(TimeSpan defaultTimeout)
|
||||||
{
|
{
|
||||||
defaultTimeoutMs = (int)defaultTimeout.TotalMilliseconds;
|
defaultTimeoutMs = (int)defaultTimeout.TotalMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GenericHandleResponse(object response, IMessageContext context)
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes incoming messages to complete the corresponding request task.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
public void HandleMessage(IMessageContext context)
|
||||||
{
|
{
|
||||||
if (context.Properties.CorrelationId == null)
|
if (context.Properties.CorrelationId == null)
|
||||||
return;
|
return;
|
||||||
@ -30,9 +41,16 @@ namespace Tapeti.Transient
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (map.TryRemove(continuationID, out var tcs))
|
if (map.TryRemove(continuationID, out var tcs))
|
||||||
tcs.SetResult(response);
|
tcs.SetResult(context.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a request and waits for the response. Do not call directly, instead use ITransientPublisher.RequestResponse.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="publisher"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<object> RequestResponse(IPublisher publisher, object request)
|
public async Task<object> RequestResponse(IPublisher publisher, object request)
|
||||||
{
|
{
|
||||||
var correlation = Guid.NewGuid();
|
var correlation = Guid.NewGuid();
|
||||||
@ -40,7 +58,7 @@ namespace Tapeti.Transient
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var properties = new BasicProperties
|
var properties = new MessageProperties
|
||||||
{
|
{
|
||||||
CorrelationId = correlation.ToString(),
|
CorrelationId = correlation.ToString(),
|
||||||
ReplyTo = TransientResponseQueueName,
|
ReplyTo = TransientResponseQueueName,
|
||||||
@ -64,6 +82,7 @@ namespace Tapeti.Transient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void TimeoutResponse(object tcs)
|
private void TimeoutResponse(object tcs)
|
||||||
{
|
{
|
||||||
((TaskCompletionSource<object>)tcs).SetException(new TimeoutException("Transient RequestResponse timed out at (ms) " + defaultTimeoutMs));
|
((TaskCompletionSource<object>)tcs).SetException(new TimeoutException("Transient RequestResponse timed out at (ms) " + defaultTimeoutMs));
|
||||||
|
BIN
Tapeti.png
Normal file
BIN
Tapeti.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
@ -1,4 +1,9 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">False</s:Boolean>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue">ID</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KV/@EntryIndexedValue">KV</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KV/@EntryIndexedValue">KV</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
89
Tapeti/Config/IBinding.cs
Normal file
89
Tapeti/Config/IBinding.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a registered binding to handle incoming messages.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the queue the binding is consuming. May change after a reconnect for dynamic queues.
|
||||||
|
/// </summary>
|
||||||
|
string QueueName { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after a connection is established to set up the binding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target"></param>
|
||||||
|
Task Apply(IBindingTarget target);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the message as specified by the message class can be handled by this binding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageClass"></param>
|
||||||
|
bool Accept(Type messageClass);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the handler for the message as specified by the context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
Task Invoke(IMessageContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the binding to specify to which queue it should bind to and how.
|
||||||
|
/// At most one of these methods can be called, calling a second method will result in an exception.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBindingTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the messageClass to the specified durable queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageClass">The message class to be bound to the queue</param>
|
||||||
|
/// <param name="queueName">The name of the durable queue</param>
|
||||||
|
Task BindDurable(Type messageClass, string queueName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the messageClass to a dynamic auto-delete queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dynamic bindings for different messageClasses will be bundled into a single dynamic queue.
|
||||||
|
/// Specifying a different queuePrefix is a way to force bindings into separate queues.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="messageClass">The message class to be bound to the queue</param>
|
||||||
|
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
|
||||||
|
/// <returns>The generated name of the dynamic queue</returns>
|
||||||
|
Task<string> BindDynamic(Type messageClass, string queuePrefix = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Declares a durable queue but does not add a binding for a messageClass' routing key.
|
||||||
|
/// Used for direct-to-queue messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName">The name of the durable queue</param>
|
||||||
|
Task BindDirectDurable(string queueName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
|
||||||
|
/// Used for direct-to-queue messages. The messageClass is used to ensure each queue only handles unique message types.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageClass">The message class which will be handled on the queue. It is not actually bound to the queue.</param>
|
||||||
|
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
|
||||||
|
/// <returns>The generated name of the dynamic queue</returns>
|
||||||
|
Task<string> BindDirectDynamic(Type messageClass = null, string queuePrefix = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
|
||||||
|
/// Used for direct-to-queue messages. Guarantees a unique queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
|
||||||
|
/// <returns>The generated name of the dynamic queue</returns>
|
||||||
|
Task<string> BindDirectDynamic(string queuePrefix = null);
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public delegate object ValueFactory(IMessageContext context);
|
|
||||||
public delegate Task ResultHandler(IMessageContext context, object value);
|
|
||||||
|
|
||||||
|
|
||||||
public enum QueueBindingMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Allow binding of the routing key from the message's source exchange to the queue
|
|
||||||
/// </summary>
|
|
||||||
RoutingKey,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Do not bind, rely on the direct-to-queue exchange
|
|
||||||
/// </summary>
|
|
||||||
DirectToQueue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IBindingContext
|
|
||||||
{
|
|
||||||
Type MessageClass { get; set; }
|
|
||||||
|
|
||||||
MethodInfo Method { get; }
|
|
||||||
IReadOnlyList<IBindingParameter> Parameters { get; }
|
|
||||||
IBindingResult Result { get; }
|
|
||||||
|
|
||||||
QueueBindingMode QueueBindingMode { get; set; }
|
|
||||||
|
|
||||||
void Use(IMessageFilterMiddleware filterMiddleware);
|
|
||||||
void Use(IMessageMiddleware middleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IBindingParameter
|
|
||||||
{
|
|
||||||
ParameterInfo Info { get; }
|
|
||||||
bool HasBinding { get; }
|
|
||||||
|
|
||||||
void SetBinding(ValueFactory valueFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IBindingResult
|
|
||||||
{
|
|
||||||
ParameterInfo Info { get; }
|
|
||||||
bool HasHandler { get; }
|
|
||||||
|
|
||||||
void SetHandler(ResultHandler resultHandler);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface IBindingMiddleware
|
|
||||||
{
|
|
||||||
void Handle(IBindingContext context, Action next);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface ICleanupMiddleware
|
|
||||||
{
|
|
||||||
Task Handle(IMessageContext context, HandlingResult handlingResult);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface IConfig
|
|
||||||
{
|
|
||||||
bool UsePublisherConfirms { get; }
|
|
||||||
|
|
||||||
IDependencyResolver DependencyResolver { get; }
|
|
||||||
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
|
||||||
IReadOnlyList<ICleanupMiddleware> CleanupMiddleware { get; }
|
|
||||||
IReadOnlyList<IPublishMiddleware> PublishMiddleware { get; }
|
|
||||||
IEnumerable<IQueue> Queues { get; }
|
|
||||||
|
|
||||||
IBinding GetBinding(Delegate method);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IQueue
|
|
||||||
{
|
|
||||||
bool Dynamic { get; }
|
|
||||||
string Name { get; }
|
|
||||||
|
|
||||||
IEnumerable<IBinding> Bindings { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IDynamicQueue : IQueue
|
|
||||||
{
|
|
||||||
string GetDeclareQueueName();
|
|
||||||
void SetName(string name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IBinding
|
|
||||||
{
|
|
||||||
Type Controller { get; }
|
|
||||||
MethodInfo Method { get; }
|
|
||||||
Type MessageClass { get; }
|
|
||||||
string QueueName { get; }
|
|
||||||
QueueBindingMode QueueBindingMode { get; set; }
|
|
||||||
|
|
||||||
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
|
|
||||||
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
|
||||||
|
|
||||||
bool Accept(Type messageClass);
|
|
||||||
bool Accept(IMessageContext context, object message);
|
|
||||||
Task Invoke(IMessageContext context, object message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IBuildBinding : IBinding
|
|
||||||
{
|
|
||||||
void SetQueueName(string queueName);
|
|
||||||
}
|
|
||||||
}
|
|
137
Tapeti/Config/IControllerBindingContext.cs
Normal file
137
Tapeti/Config/IControllerBindingContext.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Injects a value for a controller method parameter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
public delegate object ValueFactory(IControllerMessageContext context);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the return value of a controller method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public delegate Task ResultHandler(IControllerMessageContext context, object value);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines how the binding target is configured.
|
||||||
|
/// </summary>
|
||||||
|
public enum BindingTargetMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bind to a queue using the message's routing key
|
||||||
|
/// </summary>
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bind to a queue without registering the message's routing key
|
||||||
|
/// </summary>
|
||||||
|
Direct
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides information about the controller and method being registered.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerBindingContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The message class for this method.
|
||||||
|
/// </summary>
|
||||||
|
Type MessageClass { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The controller class for this binding.
|
||||||
|
/// </summary>
|
||||||
|
Type Controller { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method for this binding.
|
||||||
|
/// </summary>
|
||||||
|
MethodInfo Method { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of parameters passed to the method.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<IBindingParameter> Parameters { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The return type of the method.
|
||||||
|
/// </summary>
|
||||||
|
IBindingResult Result { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the message class for this method. Can only be called once, which is always done first by the default MessageBinding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageClass"></param>
|
||||||
|
void SetMessageClass(Type messageClass);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines how the binding target is configured. Can only be called once. Defaults to 'Default'.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode"></param>
|
||||||
|
void SetBindingTargetMode(BindingTargetMode mode);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add middleware specific to this method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler"></param>
|
||||||
|
void Use(IControllerMiddlewareBase handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about a method parameter and how it gets it's value.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBindingParameter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the reflection info for this parameter.
|
||||||
|
/// </summary>
|
||||||
|
ParameterInfo Info { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a binding has been set.
|
||||||
|
/// </summary>
|
||||||
|
bool HasBinding { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the binding for this parameter. Can only be called once.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="valueFactory"></param>
|
||||||
|
void SetBinding(ValueFactory valueFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the return type of a method.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBindingResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the reflection info for this return value.
|
||||||
|
/// </summary>
|
||||||
|
ParameterInfo Info { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a handler has been set.
|
||||||
|
/// </summary>
|
||||||
|
bool HasHandler { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the handler for this return type. Can only be called once.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resultHandler"></param>
|
||||||
|
void SetHandler(ResultHandler resultHandler);
|
||||||
|
}
|
||||||
|
}
|
19
Tapeti/Config/IControllerBindingMiddleware.cs
Normal file
19
Tapeti/Config/IControllerBindingMiddleware.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a Controller method is registered.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerBindingMiddleware : IControllerMiddlewareBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called before a Controller method is registered. Can change the way parameters and return values are handled,
|
||||||
|
/// and can inject message middleware specific to a method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="next">Must be called to activate the new layer of middleware.</param>
|
||||||
|
void Handle(IControllerBindingContext context, Action next);
|
||||||
|
}
|
||||||
|
}
|
20
Tapeti/Config/IControllerCleanupMiddleware.cs
Normal file
20
Tapeti/Config/IControllerCleanupMiddleware.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes middleware that runs after controller methods.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerCleanupMiddleware : IControllerMiddlewareBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the message handler method, even if exceptions occured.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="handlingResult"></param>
|
||||||
|
/// <param name="next">Always call to allow the next in the chain to clean up</param>
|
||||||
|
Task Cleanup(IControllerMessageContext context, HandlingResult handlingResult, Func<Task> next);
|
||||||
|
}
|
||||||
|
}
|
20
Tapeti/Config/IControllerFilterMiddleware.cs
Normal file
20
Tapeti/Config/IControllerFilterMiddleware.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes middleware that runs before the controller is instantiated.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerFilterMiddleware : IControllerMiddlewareBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called before the
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task Filter(IControllerMessageContext context, Func<Task> next);
|
||||||
|
}
|
||||||
|
}
|
37
Tapeti/Config/IControllerMessageContext.cs
Normal file
37
Tapeti/Config/IControllerMessageContext.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Extends the message context with information about the controller.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerMessageContext : IMessageContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An instance of the controller referenced by the binding.
|
||||||
|
/// </summary>
|
||||||
|
object Controller { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Provides access to the binding which is currently processing the message.
|
||||||
|
/// </remarks>
|
||||||
|
new IControllerMethodBinding Binding { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a key-value pair in the context for passing information between the various
|
||||||
|
/// controller middleware stages (IControllerMiddlewareBase descendants).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">A unique key. It is recommended to prefix it with the package name which hosts the middleware to prevent conflicts</param>
|
||||||
|
/// <param name="value">Will be disposed if the value implements IDisposable</param>
|
||||||
|
void Store(string key, object value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a previously stored value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns>True if the value was found, False otherwise</returns>
|
||||||
|
bool Get<T>(string key, out T value) where T : class;
|
||||||
|
}
|
||||||
|
}
|
19
Tapeti/Config/IControllerMessageMiddleware.cs
Normal file
19
Tapeti/Config/IControllerMessageMiddleware.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes middleware that runs for controller methods.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerMessageMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the message has passed any filter middleware and the controller has been instantiated,
|
||||||
|
/// but before the method has been called.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="next">Call to pass the message to the next handler in the chain or call the controller method</param>
|
||||||
|
Task Handle(IControllerMessageContext context, Func<Task> next);
|
||||||
|
}
|
||||||
|
}
|
22
Tapeti/Config/IControllerMethodBinding.cs
Normal file
22
Tapeti/Config/IControllerMethodBinding.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a binding to a method in a controller class to handle incoming messages.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerMethodBinding : IBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The controller class.
|
||||||
|
/// </summary>
|
||||||
|
Type Controller { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method on the Controller class to which this binding is bound.
|
||||||
|
/// </summary>
|
||||||
|
MethodInfo Method { get; }
|
||||||
|
}
|
||||||
|
}
|
9
Tapeti/Config/IControllerMiddlewareBase.cs
Normal file
9
Tapeti/Config/IControllerMiddlewareBase.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base interface for all controller middleware. Implement at least one of the descendant interfaces to be able to register.
|
||||||
|
/// </summary>
|
||||||
|
public interface IControllerMiddlewareBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface ICustomBinding
|
|
||||||
{
|
|
||||||
Type Controller { get; }
|
|
||||||
|
|
||||||
MethodInfo Method { get; }
|
|
||||||
|
|
||||||
QueueBindingMode QueueBindingMode { get; }
|
|
||||||
|
|
||||||
string StaticQueueName { get; }
|
|
||||||
|
|
||||||
string DynamicQueuePrefix { get; }
|
|
||||||
|
|
||||||
Type MessageClass { get; } // Needed to get routing key information when QueueBindingMode = RoutingKey
|
|
||||||
|
|
||||||
bool Accept(Type messageClass);
|
|
||||||
|
|
||||||
bool Accept(IMessageContext context, object message);
|
|
||||||
|
|
||||||
Task Invoke(IMessageContext context, object message);
|
|
||||||
|
|
||||||
void SetQueueName(string queueName);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using RabbitMQ.Client;
|
|
||||||
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Provides information about the message currently being handled.
|
||||||
|
/// </summary>
|
||||||
public interface IMessageContext : IDisposable
|
public interface IMessageContext : IDisposable
|
||||||
{
|
{
|
||||||
IDependencyResolver DependencyResolver { get; }
|
/// <summary>
|
||||||
|
/// Provides access to the Tapeti config.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfig Config { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the name of the queue the message was consumed from.
|
||||||
|
/// </summary>
|
||||||
string Queue { get; }
|
string Queue { get; }
|
||||||
string RoutingKey { get; }
|
|
||||||
object Message { get; }
|
|
||||||
IBasicProperties Properties { get; }
|
|
||||||
|
|
||||||
IDictionary<string, object> Items { get; }
|
/// <summary>
|
||||||
|
/// Contains the exchange to which the message was published.
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the routing key as provided when the message was published.
|
||||||
|
/// </summary>
|
||||||
|
string RoutingKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the decoded message instance.
|
||||||
|
/// </summary>
|
||||||
|
object Message { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the message metadata.
|
||||||
|
/// </summary>
|
||||||
|
IMessageProperties Properties { get; }
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Controller will be null when passed to a IMessageFilterMiddleware
|
/// Provides access to the binding which is currently processing the message.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
object Controller { get; }
|
|
||||||
|
|
||||||
IBinding Binding { get; }
|
IBinding Binding { get; }
|
||||||
|
|
||||||
IMessageContext SetupNestedContext();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface IMessageFilterMiddleware
|
|
||||||
{
|
|
||||||
Task Handle(IMessageContext context, Func<Task> next);
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,8 +3,16 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes middleware that processes all messages.
|
||||||
|
/// </summary>
|
||||||
public interface IMessageMiddleware
|
public interface IMessageMiddleware
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called for all bindings when a message needs to be handled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="next">Call to pass the message to the next handler in the chain</param>
|
||||||
Task Handle(IMessageContext context, Func<Task> next);
|
Task Handle(IMessageContext context, Func<Task> next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
Tapeti/Config/IMessageProperties.cs
Normal file
48
Tapeti/Config/IMessageProperties.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata properties attached to a message, equivalent to the RabbitMQ Client's IBasicProperties.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMessageProperties
|
||||||
|
{
|
||||||
|
/// <summary></summary>
|
||||||
|
string ContentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary></summary>
|
||||||
|
string CorrelationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary></summary>
|
||||||
|
string ReplyTo { get; set; }
|
||||||
|
|
||||||
|
/// <summary></summary>
|
||||||
|
bool? Persistent { get; set; }
|
||||||
|
|
||||||
|
/// <summary></summary>
|
||||||
|
DateTime? Timestamp { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a custom header.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
void SetHeader(string name, string value);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the value of a custom header field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <returns>The value if found, null otherwise</returns>
|
||||||
|
string GetHeader(string name);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves all custom headers.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<KeyValuePair<string, string>> GetHeaders();
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,34 @@
|
|||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to information about the message being published.
|
||||||
|
/// </summary>
|
||||||
public interface IPublishContext
|
public interface IPublishContext
|
||||||
{
|
{
|
||||||
IDependencyResolver DependencyResolver { get; }
|
/// <summary>
|
||||||
|
/// Provides access to the Tapeti config.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfig Config { get; }
|
||||||
|
|
||||||
string Exchange { get; }
|
/// <summary>
|
||||||
|
/// The exchange to which the message will be published.
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The routing key which will be included with the message.
|
||||||
|
/// </summary>
|
||||||
string RoutingKey { get; }
|
string RoutingKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instance of the message class.
|
||||||
|
/// </summary>
|
||||||
object Message { get; }
|
object Message { get; }
|
||||||
IBasicProperties Properties { get; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the message metadata.
|
||||||
|
/// </summary>
|
||||||
|
IMessageProperties Properties { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
112
Tapeti/Config/ITapetiConfig.cs
Normal file
112
Tapeti/Config/ITapetiConfig.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the Tapeti configuration.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the wrapper for an IoC container, to provide dependency injection to Tapeti.
|
||||||
|
/// </summary>
|
||||||
|
IDependencyResolver DependencyResolver { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Various Tapeti features which can be turned on or off.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfigFeatues Features { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the different kinds of registered middleware.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfigMiddleware Middleware { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of all registered bindings.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfigBindings Bindings { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Various Tapeti features which can be turned on or off.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfigFeatues
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether 'publisher confirms' are used. This RabbitMQ features allows Tapeti to
|
||||||
|
/// be notified if a message has no route, and guarantees delivery for request-response style
|
||||||
|
/// messages and those marked with the Mandatory attribute. On by default, can only be turned
|
||||||
|
/// off by explicitly calling DisablePublisherConfirms, which is not recommended.
|
||||||
|
/// </summary>
|
||||||
|
bool PublisherConfirms { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If enabled, durable queues will be created at startup and their bindings will be updated
|
||||||
|
/// with the currently registered message handlers. If not enabled all durable queues must
|
||||||
|
/// already be present when the connection is made.
|
||||||
|
/// </summary>
|
||||||
|
bool DeclareDurableQueues { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the different kinds of registered middleware.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfigMiddleware
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A list of message middleware which is called when a message is being consumed.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<IMessageMiddleware> Message { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of publish middleware which is called when a message is being published.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<IPublishMiddleware> Publish { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a list of registered bindings, with a few added helpers.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfigBindings : IReadOnlyList<IBinding>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for a binding linked to the specified method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
/// <returns>The binding if found, null otherwise</returns>
|
||||||
|
IControllerMethodBinding ForMethod(Delegate method);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
public interface IBinding
|
||||||
|
{
|
||||||
|
Type Controller { get; }
|
||||||
|
MethodInfo Method { get; }
|
||||||
|
Type MessageClass { get; }
|
||||||
|
string QueueName { get; }
|
||||||
|
QueueBindingMode QueueBindingMode { get; set; }
|
||||||
|
|
||||||
|
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
|
||||||
|
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||||
|
|
||||||
|
bool Accept(Type messageClass);
|
||||||
|
bool Accept(IMessageContext context, object message);
|
||||||
|
Task Invoke(IMessageContext context, object message);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
public interface IBuildBinding : IBinding
|
||||||
|
{
|
||||||
|
void SetQueueName(string queueName);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
116
Tapeti/Config/ITapetiConfigBuilder.cs
Normal file
116
Tapeti/Config/ITapetiConfigBuilder.cs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configures Tapeti. Every method other than Build returns the builder instance
|
||||||
|
/// for method chaining.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfigBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a locked version of the configuration which can be used to establish a connection.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfig Build();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers binding middleware which is called when a binding is created for a controller method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler"></param>
|
||||||
|
ITapetiConfigBuilder Use(IControllerBindingMiddleware handler);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers message middleware which is called to handle an incoming message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler"></param>
|
||||||
|
ITapetiConfigBuilder Use(IMessageMiddleware handler);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers publish middleware which is called when a message is published.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler"></param>
|
||||||
|
ITapetiConfigBuilder Use(IPublishMiddleware handler);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a Tapeti extension, which is a bundling mechanism for features that require more than one middleware and
|
||||||
|
/// optionally other dependency injected implementations.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="extension"></param>
|
||||||
|
ITapetiConfigBuilder Use(ITapetiExtension extension);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a binding which can accept messages. In most cases this method should not be called outside
|
||||||
|
/// of Tapeti. Instead use the RegisterAllControllers extension method to automatically create bindings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="binding"></param>
|
||||||
|
void RegisterBinding(IBinding binding);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables 'publisher confirms'. This RabbitMQ features allows Tapeti to be notified if a message
|
||||||
|
/// has no route, and guarantees delivery for request-response style messages and those marked with
|
||||||
|
/// the Mandatory attribute. On by default.
|
||||||
|
///
|
||||||
|
/// WARNING: disabling publisher confirms means there is no guarantee that a Publish succeeds,
|
||||||
|
/// and disables Tapeti.Flow from verifying if a request/response can be routed. This may
|
||||||
|
/// result in never-ending flows. Only disable if you can accept those consequences.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfigBuilder DisablePublisherConfirms();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures 'publisher confirms'. This RabbitMQ features allows Tapeti to be notified if a message
|
||||||
|
/// has no route, and guarantees delivery for request-response style messages and those marked with
|
||||||
|
/// the Mandatory attribute. On by default.
|
||||||
|
///
|
||||||
|
/// WARNING: disabling publisher confirms means there is no guarantee that a Publish succeeds,
|
||||||
|
/// and disables Tapeti.Flow from verifying if a request/response can be routed. This may
|
||||||
|
/// result in never-ending flows. Only disable if you can accept those consequences.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfigBuilder SetPublisherConfirms(bool enabled);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables the automatic creation of durable queues and updating of their bindings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Note that access to the RabbitMQ Management plugin's REST API is required for this
|
||||||
|
/// feature to work, since AMQP does not provide a way to query existing bindings.
|
||||||
|
/// </remarks>
|
||||||
|
ITapetiConfigBuilder EnableDeclareDurableQueues();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the automatic creation of durable queues and updating of their bindings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Note that access to the RabbitMQ Management plugin's REST API is required for this
|
||||||
|
/// feature to work, since AMQP does not provide a way to query existing bindings.
|
||||||
|
/// </remarks>
|
||||||
|
ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access interface for ITapetiConfigBuilder extension methods. Allows access to the registered middleware
|
||||||
|
/// before the configuration is built. Implementations of ITapetiConfigBuilder should also implement this interface.
|
||||||
|
/// Should not be used outside of Tapeti packages.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiConfigBuilderAccess
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the dependency resolver.
|
||||||
|
/// </summary>
|
||||||
|
IDependencyResolver DependencyResolver { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the currently registered binding middleware to
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="lastHandler"></param>
|
||||||
|
void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler);
|
||||||
|
}
|
||||||
|
}
|
@ -2,10 +2,23 @@
|
|||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A bundling mechanism for Tapeti extension packages. Allows the calling application to
|
||||||
|
/// pass all the necessary components to TapetiConfig.Use in one call.
|
||||||
|
/// </summary>
|
||||||
public interface ITapetiExtension
|
public interface ITapetiExtension
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the extension to register default implementations into the IoC container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="container"></param>
|
||||||
void RegisterDefaults(IDependencyContainer container);
|
void RegisterDefaults(IDependencyContainer container);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a list of middleware implementations to be passed to the TapetiConfig.Use method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dependencyResolver"></param>
|
||||||
|
/// <returns>A list of middleware implementations or null if no middleware needs to be registered</returns>
|
||||||
IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver);
|
IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
Tapeti/Config/ITapetiExtensionBinding.cs
Normal file
18
Tapeti/Config/ITapetiExtensionBinding.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a way for Tapeti extensions to register custom bindings.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiExtensionBinding : ITapetiExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Produces a list of bindings to be registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dependencyResolver"></param>
|
||||||
|
/// <returns>A list of bindings or null if no bindings need to be registered</returns>
|
||||||
|
IEnumerable<IBinding> GetBindings(IDependencyResolver dependencyResolver);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Tapeti.Config
|
|
||||||
{
|
|
||||||
public interface ITapetiExtentionBinding
|
|
||||||
{
|
|
||||||
IEnumerable<ICustomBinding> GetBindings(IDependencyResolver dependencyResolver);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,25 @@
|
|||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
public class DisconnectedEventArgs
|
/// <summary>
|
||||||
{
|
/// Receives notifications on the state of the connection.
|
||||||
public ushort ReplyCode;
|
/// </summary>
|
||||||
public string ReplyText;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IConnectionEventListener
|
public interface IConnectionEventListener
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a connection to RabbitMQ has been established.
|
||||||
|
/// </summary>
|
||||||
void Connected();
|
void Connected();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the connection to RabbitMQ has been lost.
|
||||||
|
/// </summary>
|
||||||
void Reconnected();
|
void Reconnected();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the connection to RabbitMQ has been recovered after an unexpected disconnect.
|
||||||
|
/// </summary>
|
||||||
void Disconnected(DisconnectedEventArgs e);
|
void Disconnected(DisconnectedEventArgs e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
113
Tapeti/Connection/ITapetiClient.cs
Normal file
113
Tapeti/Connection/ITapetiClient.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Connection
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="IEquatable{T}" />
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a queue binding to an exchange using a routing key
|
||||||
|
/// </summary>
|
||||||
|
public struct QueueBinding : IEquatable<QueueBinding>
|
||||||
|
{
|
||||||
|
/// <summary></summary>
|
||||||
|
public readonly string Exchange;
|
||||||
|
|
||||||
|
/// <summary></summary>
|
||||||
|
public readonly string RoutingKey;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new QueueBinding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exchange"></param>
|
||||||
|
/// <param name="routingKey"></param>
|
||||||
|
public QueueBinding(string exchange, string routingKey)
|
||||||
|
{
|
||||||
|
Exchange = exchange;
|
||||||
|
RoutingKey = routingKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Equals(QueueBinding other)
|
||||||
|
{
|
||||||
|
return string.Equals(Exchange, other.Exchange) && string.Equals(RoutingKey, other.RoutingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, obj)) return false;
|
||||||
|
return obj is QueueBinding other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
return ((Exchange != null ? Exchange.GetHashCode() : 0) * 397) ^ (RoutingKey != null ? RoutingKey.GetHashCode() : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a bridge between Tapeti and the actual RabbitMQ client
|
||||||
|
/// </summary>
|
||||||
|
public interface ITapetiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes a message. The exchange and routing key are determined by the registered strategies.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="body">The raw message data to publish</param>
|
||||||
|
/// <param name="properties">Metadata to include in the message</param>
|
||||||
|
/// <param name="exchange">The exchange to publish the message to, or empty to send it directly to a queue</param>
|
||||||
|
/// <param name="routingKey">The routing key for the message, or queue name if exchange is empty</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(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts a consumer for the specified queue, using the provided bindings to handle messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName"></param>
|
||||||
|
/// <param name="consumer">The consumer implementation which will receive the messages from the queue</param>
|
||||||
|
Task Consume(string queueName, IConsumer consumer);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a durable queue if it does not already exist, and updates the bindings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName">The name of the queue to create</param>
|
||||||
|
/// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param>
|
||||||
|
Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies a durable queue exists. Will raise an exception if it does not.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName">The name of the queue to verify</param>
|
||||||
|
Task DurableQueueVerify(string queueName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
|
||||||
|
Task<string> DynamicQueueDeclare(string queuePrefix = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a binding to a dynamic queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName">The name of the dynamic queue previously created using DynamicQueueDeclare</param>
|
||||||
|
/// <param name="binding">The binding to add to the dynamic queue</param>
|
||||||
|
Task DynamicQueueBind(string queueName, QueueBinding binding);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the connection to RabbitMQ gracefully.
|
||||||
|
/// </summary>
|
||||||
|
Task Close();
|
||||||
|
}
|
||||||
|
}
|
43
Tapeti/Connection/TapetiBasicConsumer.cs
Normal file
43
Tapeti/Connection/TapetiBasicConsumer.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using Tapeti.Default;
|
||||||
|
|
||||||
|
namespace Tapeti.Connection
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the bridge between the RabbitMQ Client consumer and a Tapeti Consumer
|
||||||
|
/// </summary>
|
||||||
|
public class TapetiBasicConsumer : DefaultBasicConsumer
|
||||||
|
{
|
||||||
|
private readonly IConsumer consumer;
|
||||||
|
private readonly Func<ulong, ConsumeResponse, Task> onRespond;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TapetiBasicConsumer(IConsumer consumer, Func<ulong, ConsumeResponse, Task> onRespond)
|
||||||
|
{
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.onRespond = onRespond;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, IBasicProperties properties, byte[] body)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await consumer.Consume(exchange, routingKey, new RabbitMQMessageProperties(properties), body);
|
||||||
|
await onRespond(deliveryTag, response);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await onRespond(deliveryTag, ConsumeResponse.Nack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +1,45 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
using RabbitMQ.Client.Exceptions;
|
using RabbitMQ.Client.Exceptions;
|
||||||
using RabbitMQ.Client.Framing;
|
using RabbitMQ.Client.Framing;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Default;
|
||||||
using Tapeti.Exceptions;
|
using Tapeti.Exceptions;
|
||||||
using Tapeti.Helpers;
|
|
||||||
using Tapeti.Tasks;
|
using Tapeti.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
public class TapetiWorker
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of ITapetiClient for the RabbitMQ Client library
|
||||||
|
/// </summary>
|
||||||
|
public class TapetiClient : ITapetiClient
|
||||||
{
|
{
|
||||||
private const int ReconnectDelay = 5000;
|
private const int ReconnectDelay = 5000;
|
||||||
private const int MandatoryReturnTimeout = 30000;
|
private const int MandatoryReturnTimeout = 30000;
|
||||||
private const int MinimumConnectedReconnectDelay = 1000;
|
private const int MinimumConnectedReconnectDelay = 1000;
|
||||||
|
|
||||||
private readonly IConfig config;
|
private readonly TapetiConnectionParams connectionParams;
|
||||||
|
|
||||||
|
private readonly ITapetiConfig config;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
public TapetiConnectionParams ConnectionParams { get; set; }
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receives events when the connection state changes.
|
||||||
|
/// </summary>
|
||||||
public IConnectionEventListener ConnectionEventListener { get; set; }
|
public IConnectionEventListener ConnectionEventListener { get; set; }
|
||||||
|
|
||||||
private readonly IMessageSerializer messageSerializer;
|
|
||||||
private readonly IRoutingKeyStrategy routingKeyStrategy;
|
|
||||||
private readonly IExchangeStrategy exchangeStrategy;
|
|
||||||
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
|
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +50,7 @@ namespace Tapeti.Connection
|
|||||||
private IModel channelInstance;
|
private IModel channelInstance;
|
||||||
private ulong lastDeliveryTag;
|
private ulong lastDeliveryTag;
|
||||||
private DateTime connectedDateTime;
|
private DateTime connectedDateTime;
|
||||||
|
private HttpClient managementClient;
|
||||||
|
|
||||||
// These fields must be locked, since the callbacks for BasicAck/BasicReturn can run in a different thread
|
// These fields must be locked, since the callbacks for BasicAck/BasicReturn can run in a different thread
|
||||||
private readonly object confirmLock = new object();
|
private readonly object confirmLock = new object();
|
||||||
@ -60,88 +72,123 @@ namespace Tapeti.Connection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TapetiWorker(IConfig config)
|
public TapetiClient(ITapetiConfig config, TapetiConnectionParams connectionParams)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.connectionParams = connectionParams;
|
||||||
|
|
||||||
logger = config.DependencyResolver.Resolve<ILogger>();
|
logger = config.DependencyResolver.Resolve<ILogger>();
|
||||||
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
|
|
||||||
routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>();
|
|
||||||
exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task Publish(object message, IBasicProperties properties, bool mandatory)
|
var handler = new HttpClientHandler
|
||||||
{
|
{
|
||||||
return Publish(message, properties, exchangeStrategy.GetExchange(message.GetType()), routingKeyStrategy.GetRoutingKey(message.GetType()), mandatory);
|
Credentials = new NetworkCredential(connectionParams.Username, connectionParams.Password)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
managementClient = new HttpClient(handler)
|
||||||
public Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory)
|
|
||||||
{
|
{
|
||||||
return Publish(message, properties, "", queueName, mandatory);
|
Timeout = TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
managementClient.DefaultRequestHeaders.Add("Connection", "close");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Consume(string queueName, IEnumerable<IBinding> bindings)
|
/// <inheritdoc />
|
||||||
|
public async Task Publish(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory)
|
||||||
|
{
|
||||||
|
var publishProperties = new RabbitMQMessageProperties(new BasicProperties(), properties);
|
||||||
|
|
||||||
|
await taskQueue.Value.Add(async () =>
|
||||||
|
{
|
||||||
|
Task<int> publishResultTask = null;
|
||||||
|
var messageInfo = new ConfirmMessageInfo
|
||||||
|
{
|
||||||
|
ReturnKey = GetReturnKey(exchange, routingKey),
|
||||||
|
CompletionSource = new TaskCompletionSource<int>()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
WithRetryableChannel(channel =>
|
||||||
|
{
|
||||||
|
// The delivery tag is lost after a reconnect, register under the new tag
|
||||||
|
if (config.Features.PublisherConfirms)
|
||||||
|
{
|
||||||
|
lastDeliveryTag++;
|
||||||
|
|
||||||
|
Monitor.Enter(confirmLock);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
confirmMessages.Add(lastDeliveryTag, messageInfo);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Exit(confirmLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
publishResultTask = messageInfo.CompletionSource.Task;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
mandatory = false;
|
||||||
|
|
||||||
|
channel.BasicPublish(exchange, routingKey, mandatory, publishProperties.BasicProperties, body);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (publishResultTask == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var delayCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
var signalledTask = await Task.WhenAny(
|
||||||
|
publishResultTask,
|
||||||
|
Task.Delay(MandatoryReturnTimeout, delayCancellationTokenSource.Token));
|
||||||
|
|
||||||
|
if (signalledTask != publishResultTask)
|
||||||
|
throw new TimeoutException(
|
||||||
|
$"Timeout while waiting for basic.return for message with exchange '{exchange}' and routing key '{routingKey}'");
|
||||||
|
|
||||||
|
delayCancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
if (publishResultTask.IsCanceled)
|
||||||
|
throw new NackException(
|
||||||
|
$"Mandatory message with with exchange '{exchange}' and routing key '{routingKey}' was nacked");
|
||||||
|
|
||||||
|
var replyCode = publishResultTask.Result;
|
||||||
|
|
||||||
|
// There is no RabbitMQ.Client.Framing.Constants value for this "No route" reply code
|
||||||
|
// at the time of writing...
|
||||||
|
if (replyCode == 312)
|
||||||
|
throw new NoRouteException(
|
||||||
|
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' does not have a route");
|
||||||
|
|
||||||
|
if (replyCode > 0)
|
||||||
|
throw new NoRouteException(
|
||||||
|
$"Mandatory message with exchange '{exchange}' and routing key '{routingKey}' could not be delivered, reply code: {replyCode}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Consume(string queueName, IConsumer consumer)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(queueName))
|
if (string.IsNullOrEmpty(queueName))
|
||||||
throw new ArgumentNullException(nameof(queueName));
|
throw new ArgumentNullException(nameof(queueName));
|
||||||
|
|
||||||
return taskQueue.Value.Add(() =>
|
await taskQueue.Value.Add(() =>
|
||||||
{
|
|
||||||
WithRetryableChannel(channel => channel.BasicConsume(queueName, false, new TapetiConsumer(this, queueName, config.DependencyResolver, bindings, config.MessageMiddleware, config.CleanupMiddleware)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task Subscribe(IQueue queue)
|
|
||||||
{
|
|
||||||
return taskQueue.Value.Add(() =>
|
|
||||||
{
|
{
|
||||||
WithRetryableChannel(channel =>
|
WithRetryableChannel(channel =>
|
||||||
{
|
{
|
||||||
if (queue.Dynamic)
|
var basicConsumer = new TapetiBasicConsumer(consumer, Respond);
|
||||||
{
|
channel.BasicConsume(queueName, false, basicConsumer);
|
||||||
if (!(queue is IDynamicQueue dynamicQueue))
|
|
||||||
throw new NullReferenceException("Queue with Dynamic = true must implement IDynamicQueue");
|
|
||||||
|
|
||||||
var declaredQueue = channel.QueueDeclare(dynamicQueue.GetDeclareQueueName());
|
|
||||||
dynamicQueue.SetName(declaredQueue.QueueName);
|
|
||||||
|
|
||||||
foreach (var binding in queue.Bindings)
|
|
||||||
{
|
|
||||||
if (binding.QueueBindingMode == QueueBindingMode.RoutingKey)
|
|
||||||
{
|
|
||||||
if (binding.MessageClass == null)
|
|
||||||
throw new NullReferenceException("Binding with QueueBindingMode = RoutingKey must have a MessageClass");
|
|
||||||
|
|
||||||
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
|
|
||||||
var exchange = exchangeStrategy.GetExchange(binding.MessageClass);
|
|
||||||
|
|
||||||
channel.QueueBind(declaredQueue.QueueName, exchange, routingKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
(binding as IBuildBinding)?.SetQueueName(declaredQueue.QueueName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
channel.QueueDeclarePassive(queue.Name);
|
|
||||||
foreach (var binding in queue.Bindings)
|
|
||||||
{
|
|
||||||
(binding as IBuildBinding)?.SetQueueName(queue.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Respond(ulong deliveryTag, ConsumeResponse response)
|
private async Task Respond(ulong deliveryTag, ConsumeResponse response)
|
||||||
{
|
{
|
||||||
return taskQueue.Value.Add(() =>
|
await taskQueue.Value.Add(() =>
|
||||||
{
|
{
|
||||||
// No need for a retryable channel here, if the connection is lost we can't
|
// No need for a retryable channel here, if the connection is lost we can't
|
||||||
// use the deliveryTag anymore.
|
// use the deliveryTag anymore.
|
||||||
@ -167,12 +214,82 @@ namespace Tapeti.Connection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Close()
|
/// <inheritdoc />
|
||||||
|
public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings)
|
||||||
|
{
|
||||||
|
await taskQueue.Value.Add(async () =>
|
||||||
|
{
|
||||||
|
var existingBindings = await GetQueueBindings(queueName);
|
||||||
|
|
||||||
|
WithRetryableChannel(channel =>
|
||||||
|
{
|
||||||
|
channel.QueueDeclare(queueName, true, false, false);
|
||||||
|
|
||||||
|
var currentBindings = bindings.ToList();
|
||||||
|
|
||||||
|
foreach (var binding in currentBindings)
|
||||||
|
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
|
||||||
|
|
||||||
|
foreach (var deletedBinding in existingBindings.Where(binding => !currentBindings.Any(b => b.Exchange == binding.Exchange && b.RoutingKey == binding.RoutingKey)))
|
||||||
|
channel.QueueUnbind(queueName, deletedBinding.Exchange, deletedBinding.RoutingKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DurableQueueVerify(string queueName)
|
||||||
|
{
|
||||||
|
await taskQueue.Value.Add(() =>
|
||||||
|
{
|
||||||
|
WithRetryableChannel(channel =>
|
||||||
|
{
|
||||||
|
channel.QueueDeclarePassive(queueName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string> DynamicQueueDeclare(string queuePrefix = null)
|
||||||
|
{
|
||||||
|
string queueName = null;
|
||||||
|
|
||||||
|
await taskQueue.Value.Add(() =>
|
||||||
|
{
|
||||||
|
WithRetryableChannel(channel =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(queuePrefix))
|
||||||
|
{
|
||||||
|
queueName = queuePrefix + "." + Guid.NewGuid().ToString("N");
|
||||||
|
channel.QueueDeclare(queueName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
queueName = channel.QueueDeclare().QueueName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return queueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DynamicQueueBind(string queueName, QueueBinding binding)
|
||||||
|
{
|
||||||
|
await taskQueue.Value.Add(() =>
|
||||||
|
{
|
||||||
|
WithRetryableChannel(channel =>
|
||||||
|
{
|
||||||
|
channel.QueueBind(queueName, binding.Exchange, binding.RoutingKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Close()
|
||||||
{
|
{
|
||||||
if (!taskQueue.IsValueCreated)
|
if (!taskQueue.IsValueCreated)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
return taskQueue.Value.Add(() =>
|
await taskQueue.Value.Add(() =>
|
||||||
{
|
{
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
|
|
||||||
@ -194,94 +311,95 @@ namespace Tapeti.Connection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Task Publish(object message, IBasicProperties properties, string exchange, string routingKey, bool mandatory)
|
private static readonly List<HttpStatusCode> TransientStatusCodes = new List<HttpStatusCode>
|
||||||
{
|
{
|
||||||
var context = new PublishContext
|
HttpStatusCode.GatewayTimeout,
|
||||||
{
|
HttpStatusCode.RequestTimeout,
|
||||||
DependencyResolver = config.DependencyResolver,
|
HttpStatusCode.ServiceUnavailable
|
||||||
Exchange = exchange,
|
|
||||||
RoutingKey = routingKey,
|
|
||||||
Message = message,
|
|
||||||
Properties = properties ?? new BasicProperties()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!context.Properties.IsTimestampPresent())
|
private static readonly TimeSpan[] ExponentialBackoff =
|
||||||
context.Properties.Timestamp = new AmqpTimestamp(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds());
|
|
||||||
|
|
||||||
if (!context.Properties.IsDeliveryModePresent())
|
|
||||||
context.Properties.DeliveryMode = 2; // Persistent
|
|
||||||
|
|
||||||
|
|
||||||
// ReSharper disable ImplicitlyCapturedClosure - MiddlewareHelper will not keep a reference to the lambdas
|
|
||||||
return MiddlewareHelper.GoAsync(
|
|
||||||
config.PublishMiddleware,
|
|
||||||
async (handler, next) => await handler.Handle(context, next),
|
|
||||||
() => taskQueue.Value.Add(async () =>
|
|
||||||
{
|
{
|
||||||
if (Thread.CurrentThread.ManagedThreadId != 3)
|
TimeSpan.FromSeconds(1),
|
||||||
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
|
TimeSpan.FromSeconds(2),
|
||||||
|
TimeSpan.FromSeconds(3),
|
||||||
var body = messageSerializer.Serialize(context.Message, context.Properties);
|
TimeSpan.FromSeconds(5),
|
||||||
|
TimeSpan.FromSeconds(8),
|
||||||
Task<int> publishResultTask = null;
|
TimeSpan.FromSeconds(13),
|
||||||
var messageInfo = new ConfirmMessageInfo
|
TimeSpan.FromSeconds(21),
|
||||||
{
|
TimeSpan.FromSeconds(34),
|
||||||
ReturnKey = GetReturnKey(context.Exchange, context.RoutingKey),
|
TimeSpan.FromSeconds(55)
|
||||||
CompletionSource = new TaskCompletionSource<int>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
WithRetryableChannel(channel =>
|
private class ManagementBinding
|
||||||
{
|
{
|
||||||
// The delivery tag is lost after a reconnect, register under the new tag
|
[JsonProperty("source")]
|
||||||
if (config.UsePublisherConfirms)
|
public string Source { get; set; }
|
||||||
{
|
|
||||||
lastDeliveryTag++;
|
|
||||||
|
|
||||||
Monitor.Enter(confirmLock);
|
[JsonProperty("vhost")]
|
||||||
|
public string Vhost { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("destination")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("destination_type")]
|
||||||
|
public string DestinationType { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("routing_key")]
|
||||||
|
public string RoutingKey { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("arguments")]
|
||||||
|
public Dictionary<string, string> Arguments { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("properties_key")]
|
||||||
|
public string PropertiesKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task<IEnumerable<QueueBinding>> GetQueueBindings(string queueName)
|
||||||
|
{
|
||||||
|
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
|
||||||
|
var queuePath = Uri.EscapeDataString(queueName);
|
||||||
|
var requestUri = new Uri($"{connectionParams.HostName}:{connectionParams.Port}/api/queues/{virtualHostPath}/{queuePath}/bindings");
|
||||||
|
|
||||||
|
using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))
|
||||||
|
{
|
||||||
|
var retryDelayIndex = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
confirmMessages.Add(lastDeliveryTag, messageInfo);
|
var response = await managementClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var bindings = JsonConvert.DeserializeObject<IEnumerable<ManagementBinding>>(content);
|
||||||
|
|
||||||
|
// Filter out the binding to an empty source, which is always present for direct-to-queue routing
|
||||||
|
return bindings
|
||||||
|
.Where(binding => !string.IsNullOrEmpty(binding.Source))
|
||||||
|
.Select(binding => new QueueBinding(binding.Source, binding.RoutingKey));
|
||||||
}
|
}
|
||||||
finally
|
catch (TimeoutException)
|
||||||
{
|
{
|
||||||
Monitor.Exit(confirmLock);
|
}
|
||||||
|
catch (WebException e)
|
||||||
|
{
|
||||||
|
if (!(e.Response is HttpWebResponse response))
|
||||||
|
throw;
|
||||||
|
|
||||||
|
if (!TransientStatusCodes.Contains(response.StatusCode))
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
publishResultTask = messageInfo.CompletionSource.Task;
|
await Task.Delay(ExponentialBackoff[retryDelayIndex]);
|
||||||
|
|
||||||
|
if (retryDelayIndex < ExponentialBackoff.Length - 1)
|
||||||
|
retryDelayIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
mandatory = false;
|
|
||||||
|
|
||||||
channel.BasicPublish(context.Exchange, context.RoutingKey, mandatory, context.Properties, body);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (publishResultTask == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var delayCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
var signalledTask = await Task.WhenAny(publishResultTask, Task.Delay(MandatoryReturnTimeout, delayCancellationTokenSource.Token));
|
|
||||||
|
|
||||||
if (signalledTask != publishResultTask)
|
|
||||||
throw new TimeoutException($"Timeout while waiting for basic.return for message with class {context.Message?.GetType().FullName ?? "null"} and Id {context.Properties.MessageId}");
|
|
||||||
|
|
||||||
delayCancellationTokenSource.Cancel();
|
|
||||||
|
|
||||||
if (publishResultTask.IsCanceled)
|
|
||||||
throw new NackException($"Mandatory message with class {context.Message?.GetType().FullName ?? "null"} was nacked");
|
|
||||||
|
|
||||||
var replyCode = publishResultTask.Result;
|
|
||||||
|
|
||||||
// There is no RabbitMQ.Client.Framing.Constants value for this "No route" reply code
|
|
||||||
// at the time of writing...
|
|
||||||
if (replyCode == 312)
|
|
||||||
throw new NoRouteException($"Mandatory message with class {context.Message?.GetType().FullName ?? "null"} does not have a route");
|
|
||||||
|
|
||||||
if (replyCode > 0)
|
|
||||||
throw new NoRouteException($"Mandatory message with class {context.Message?.GetType().FullName ?? "null"} could not be delivery, reply code {replyCode}");
|
|
||||||
}));
|
|
||||||
// ReSharper restore ImplicitlyCapturedClosure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -298,9 +416,8 @@ namespace Tapeti.Connection
|
|||||||
operation(GetChannel());
|
operation(GetChannel());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (AlreadyClosedException e)
|
catch (AlreadyClosedException)
|
||||||
{
|
{
|
||||||
// TODO log?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,11 +440,11 @@ namespace Tapeti.Connection
|
|||||||
|
|
||||||
var connectionFactory = new ConnectionFactory
|
var connectionFactory = new ConnectionFactory
|
||||||
{
|
{
|
||||||
HostName = ConnectionParams.HostName,
|
HostName = connectionParams.HostName,
|
||||||
Port = ConnectionParams.Port,
|
Port = connectionParams.Port,
|
||||||
VirtualHost = ConnectionParams.VirtualHost,
|
VirtualHost = connectionParams.VirtualHost,
|
||||||
UserName = ConnectionParams.Username,
|
UserName = connectionParams.Username,
|
||||||
Password = ConnectionParams.Password,
|
Password = connectionParams.Password,
|
||||||
AutomaticRecoveryEnabled = false,
|
AutomaticRecoveryEnabled = false,
|
||||||
TopologyRecoveryEnabled = false,
|
TopologyRecoveryEnabled = false,
|
||||||
RequestedHeartbeat = 30
|
RequestedHeartbeat = 30
|
||||||
@ -337,7 +454,7 @@ namespace Tapeti.Connection
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.Connect(ConnectionParams);
|
logger.Connect(connectionParams);
|
||||||
|
|
||||||
connection = connectionFactory.CreateConnection();
|
connection = connectionFactory.CreateConnection();
|
||||||
channelInstance = connection.CreateModel();
|
channelInstance = connection.CreateModel();
|
||||||
@ -345,13 +462,16 @@ namespace Tapeti.Connection
|
|||||||
if (channelInstance == null)
|
if (channelInstance == null)
|
||||||
throw new BrokerUnreachableException(null);
|
throw new BrokerUnreachableException(null);
|
||||||
|
|
||||||
if (config.UsePublisherConfirms)
|
if (config.Features.PublisherConfirms)
|
||||||
{
|
{
|
||||||
lastDeliveryTag = 0;
|
lastDeliveryTag = 0;
|
||||||
|
|
||||||
Monitor.Enter(confirmLock);
|
Monitor.Enter(confirmLock);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
foreach (var pair in confirmMessages)
|
||||||
|
pair.Value.CompletionSource.SetCanceled();
|
||||||
|
|
||||||
confirmMessages.Clear();
|
confirmMessages.Clear();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -362,8 +482,8 @@ namespace Tapeti.Connection
|
|||||||
channelInstance.ConfirmSelect();
|
channelInstance.ConfirmSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConnectionParams.PrefetchCount > 0)
|
if (connectionParams.PrefetchCount > 0)
|
||||||
channelInstance.BasicQos(0, ConnectionParams.PrefetchCount, false);
|
channelInstance.BasicQos(0, connectionParams.PrefetchCount, false);
|
||||||
|
|
||||||
channelInstance.ModelShutdown += (sender, e) =>
|
channelInstance.ModelShutdown += (sender, e) =>
|
||||||
{
|
{
|
||||||
@ -390,14 +510,14 @@ namespace Tapeti.Connection
|
|||||||
else
|
else
|
||||||
ConnectionEventListener?.Connected();
|
ConnectionEventListener?.Connected();
|
||||||
|
|
||||||
logger.ConnectSuccess(ConnectionParams);
|
logger.ConnectSuccess(connectionParams);
|
||||||
isReconnect = true;
|
isReconnect = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (BrokerUnreachableException e)
|
catch (BrokerUnreachableException e)
|
||||||
{
|
{
|
||||||
logger.ConnectFailed(ConnectionParams, e);
|
logger.ConnectFailed(connectionParams, e);
|
||||||
Thread.Sleep(ReconnectDelay);
|
Thread.Sleep(ReconnectDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -507,15 +627,5 @@ namespace Tapeti.Connection
|
|||||||
{
|
{
|
||||||
return exchange + ':' + routingKey;
|
return exchange + ':' + routingKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class PublishContext : IPublishContext
|
|
||||||
{
|
|
||||||
public IDependencyResolver DependencyResolver { get; set; }
|
|
||||||
public string Exchange { get; set; }
|
|
||||||
public string RoutingKey { get; set; }
|
|
||||||
public object Message { get; set; }
|
|
||||||
public IBasicProperties Properties { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,61 +1,68 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using RabbitMQ.Client;
|
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Default;
|
using Tapeti.Default;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Helpers;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
public class TapetiConsumer : DefaultBasicConsumer
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a RabbitMQ consumer to pass messages to the Tapeti middleware.
|
||||||
|
/// </summary>
|
||||||
|
public class TapetiConsumer : IConsumer
|
||||||
{
|
{
|
||||||
private readonly TapetiWorker worker;
|
private readonly ITapetiConfig config;
|
||||||
private readonly string queueName;
|
private readonly string queueName;
|
||||||
private readonly IDependencyResolver dependencyResolver;
|
|
||||||
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
|
||||||
private readonly IReadOnlyList<ICleanupMiddleware> cleanupMiddleware;
|
|
||||||
private readonly List<IBinding> bindings;
|
private readonly List<IBinding> bindings;
|
||||||
|
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly IExceptionStrategy exceptionStrategy;
|
private readonly IExceptionStrategy exceptionStrategy;
|
||||||
|
private readonly IMessageSerializer messageSerializer;
|
||||||
|
|
||||||
|
|
||||||
public TapetiConsumer(TapetiWorker worker, string queueName, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware, IReadOnlyList<ICleanupMiddleware> cleanupMiddleware)
|
/// <inheritdoc />
|
||||||
|
public TapetiConsumer(ITapetiConfig config, string queueName, IEnumerable<IBinding> bindings)
|
||||||
{
|
{
|
||||||
this.worker = worker;
|
this.config = config;
|
||||||
this.queueName = queueName;
|
this.queueName = queueName;
|
||||||
this.dependencyResolver = dependencyResolver;
|
|
||||||
this.messageMiddleware = messageMiddleware;
|
|
||||||
this.cleanupMiddleware = cleanupMiddleware;
|
|
||||||
this.bindings = bindings.ToList();
|
this.bindings = bindings.ToList();
|
||||||
|
|
||||||
logger = dependencyResolver.Resolve<ILogger>();
|
logger = config.DependencyResolver.Resolve<ILogger>();
|
||||||
exceptionStrategy = dependencyResolver.Resolve<IExceptionStrategy>();
|
exceptionStrategy = config.DependencyResolver.Resolve<IExceptionStrategy>();
|
||||||
|
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey,
|
/// <inheritdoc />
|
||||||
IBasicProperties properties, byte[] body)
|
public async Task<ConsumeResponse> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
MessageContext context = null;
|
|
||||||
HandlingResult handlingResult = null;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context = new MessageContext
|
var message = messageSerializer.Deserialize(body, properties);
|
||||||
|
if (message == null)
|
||||||
|
throw new ArgumentException($"Message body could not be deserialized into a message object in queue {queueName}", nameof(body));
|
||||||
|
|
||||||
|
await DispatchMessage(message, new MessageContextData
|
||||||
{
|
{
|
||||||
DependencyResolver = dependencyResolver,
|
Exchange = exchange,
|
||||||
Queue = queueName,
|
|
||||||
RoutingKey = routingKey,
|
RoutingKey = routingKey,
|
||||||
Properties = properties
|
Properties = properties
|
||||||
};
|
});
|
||||||
|
|
||||||
await DispatchMesage(context, body);
|
return ConsumeResponse.Ack;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// TODO exception strategy
|
||||||
|
// TODO logger
|
||||||
|
return ConsumeResponse.Nack;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
handlingResult = new HandlingResult
|
handlingResult = new HandlingResult
|
||||||
{
|
{
|
||||||
@ -109,7 +116,7 @@ namespace Tapeti.Connection
|
|||||||
MessageAction = MessageAction.None
|
MessageAction = MessageAction.None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await worker.Respond(deliveryTag, handlingResult.ConsumeResponse);
|
await client.Respond(deliveryTag, handlingResult.ConsumeResponse);
|
||||||
}
|
}
|
||||||
catch (Exception eRespond)
|
catch (Exception eRespond)
|
||||||
{
|
{
|
||||||
@ -124,186 +131,60 @@ namespace Tapeti.Connection
|
|||||||
logger.HandlerException(eDispose);
|
logger.HandlerException(eDispose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
|
||||||
{
|
|
||||||
foreach(var handler in cleanupMiddleware)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await handler.Handle(context, handlingResult);
|
|
||||||
}
|
|
||||||
catch (Exception eCleanup)
|
|
||||||
{
|
|
||||||
logger.HandlerException(eCleanup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DispatchMesage(MessageContext context, byte[] body)
|
private async Task DispatchMessage(object message, MessageContextData messageContextData)
|
||||||
{
|
{
|
||||||
var message = dependencyResolver.Resolve<IMessageSerializer>().Deserialize(body, context.Properties);
|
var messageType = message.GetType();
|
||||||
if (message == null)
|
|
||||||
throw new ArgumentException("Empty message");
|
|
||||||
|
|
||||||
context.Message = message;
|
|
||||||
|
|
||||||
var validMessageType = false;
|
var validMessageType = false;
|
||||||
|
|
||||||
foreach (var binding in bindings)
|
foreach (var binding in bindings)
|
||||||
{
|
{
|
||||||
if (binding.Accept(context, message))
|
if (!binding.Accept(messageType))
|
||||||
{
|
continue;
|
||||||
await InvokeUsingBinding(context, binding, message);
|
|
||||||
|
|
||||||
|
await InvokeUsingBinding(message, messageContextData, binding);
|
||||||
validMessageType = true;
|
validMessageType = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!validMessageType)
|
if (!validMessageType)
|
||||||
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
|
throw new ArgumentException($"Unsupported message type in queue {queueName}: {message.GetType().FullName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task InvokeUsingBinding(MessageContext context, IBinding binding, object message)
|
|
||||||
|
private async Task InvokeUsingBinding(object message, MessageContextData messageContextData, IBinding binding)
|
||||||
{
|
{
|
||||||
context.Binding = binding;
|
var context = new MessageContext
|
||||||
|
|
||||||
RecursiveCaller firstCaller = null;
|
|
||||||
RecursiveCaller currentCaller = null;
|
|
||||||
|
|
||||||
void AddHandler(Handler handle)
|
|
||||||
{
|
{
|
||||||
var caller = new RecursiveCaller(handle);
|
Config = config,
|
||||||
if (currentCaller == null)
|
Queue = queueName,
|
||||||
firstCaller = caller;
|
Exchange = messageContextData.Exchange,
|
||||||
else
|
RoutingKey = messageContextData.RoutingKey,
|
||||||
currentCaller.Next = caller;
|
Message = message,
|
||||||
currentCaller = caller;
|
Properties = messageContextData.Properties,
|
||||||
}
|
Binding = binding
|
||||||
|
};
|
||||||
if (binding.MessageFilterMiddleware != null)
|
|
||||||
{
|
|
||||||
foreach (var m in binding.MessageFilterMiddleware)
|
|
||||||
{
|
|
||||||
AddHandler(m.Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddHandler(async (c, next) =>
|
|
||||||
{
|
|
||||||
c.Controller = dependencyResolver.Resolve(binding.Controller);
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var m in messageMiddleware)
|
|
||||||
{
|
|
||||||
AddHandler(m.Handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.MessageMiddleware != null)
|
|
||||||
{
|
|
||||||
foreach (var m in binding.MessageMiddleware)
|
|
||||||
{
|
|
||||||
AddHandler(m.Handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddHandler(async (c, next) =>
|
|
||||||
{
|
|
||||||
await binding.Invoke(c, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
return firstCaller.Call(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Exception UnwrapException(Exception exception)
|
|
||||||
{
|
|
||||||
// In async/await style code this is handled similarly. For synchronous
|
|
||||||
// code using Tasks we have to unwrap these ourselves to get the proper
|
|
||||||
// exception directly instead of "Errors occured". We might lose
|
|
||||||
// some stack traces in the process though.
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var aggregateException = exception as AggregateException;
|
|
||||||
if (aggregateException == null || aggregateException.InnerExceptions.Count != 1)
|
|
||||||
return exception;
|
|
||||||
|
|
||||||
exception = aggregateException.InnerExceptions[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate Task Handler(MessageContext context, Func<Task> next);
|
|
||||||
|
|
||||||
public class RecursiveCaller
|
|
||||||
{
|
|
||||||
private readonly Handler handle;
|
|
||||||
private MessageContext currentContext;
|
|
||||||
private MessageContext nextContext;
|
|
||||||
|
|
||||||
public RecursiveCaller Next;
|
|
||||||
|
|
||||||
public RecursiveCaller(Handler handle)
|
|
||||||
{
|
|
||||||
this.handle = handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task Call(MessageContext context)
|
|
||||||
{
|
|
||||||
if (currentContext != null)
|
|
||||||
throw new InvalidOperationException("Cannot simultaneously call 'next' in Middleware.");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
currentContext = context;
|
await MiddlewareHelper.GoAsync(config.Middleware.Message,
|
||||||
|
(handler, next) => handler.Handle(context, next),
|
||||||
context.UseNestedContext = Next == null ? (Action<MessageContext>)null : UseNestedContext;
|
async () => { await binding.Invoke(context); });
|
||||||
|
|
||||||
await handle(context, CallNext);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
currentContext = null;
|
context.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CallNext()
|
|
||||||
{
|
|
||||||
if (Next == null)
|
|
||||||
return;
|
|
||||||
if (nextContext != null)
|
|
||||||
{
|
|
||||||
await Next.Call(nextContext);
|
|
||||||
}else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Next.Call(currentContext);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
currentContext.UseNestedContext = UseNestedContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UseNestedContext(MessageContext context)
|
private struct MessageContextData
|
||||||
{
|
{
|
||||||
if (nextContext != null)
|
public string Exchange;
|
||||||
throw new InvalidOperationException("Previous nested context was not yet disposed.");
|
public string RoutingKey;
|
||||||
|
public IMessageProperties Properties;
|
||||||
context.OnContextDisposed = OnContextDisposed;
|
|
||||||
nextContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnContextDisposed(MessageContext context)
|
|
||||||
{
|
|
||||||
context.OnContextDisposed = null;
|
|
||||||
if (nextContext == context)
|
|
||||||
nextContext = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,92 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using Tapeti.Annotations;
|
using Tapeti.Annotations;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Default;
|
||||||
|
using Tapeti.Exceptions;
|
||||||
|
using Tapeti.Helpers;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public class TapetiPublisher : IInternalPublisher
|
public class TapetiPublisher : IInternalPublisher
|
||||||
{
|
{
|
||||||
private readonly Func<TapetiWorker> workerFactory;
|
private readonly ITapetiConfig config;
|
||||||
|
private readonly Func<ITapetiClient> clientFactory;
|
||||||
|
private readonly IExchangeStrategy exchangeStrategy;
|
||||||
|
private readonly IRoutingKeyStrategy routingKeyStrategy;
|
||||||
|
private readonly IMessageSerializer messageSerializer;
|
||||||
|
|
||||||
|
|
||||||
public TapetiPublisher(Func<TapetiWorker> workerFactory)
|
/// <inheritdoc />
|
||||||
|
public TapetiPublisher(ITapetiConfig config, Func<ITapetiClient> clientFactory)
|
||||||
{
|
{
|
||||||
this.workerFactory = workerFactory;
|
this.config = config;
|
||||||
|
this.clientFactory = clientFactory;
|
||||||
|
|
||||||
|
exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>();
|
||||||
|
routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>();
|
||||||
|
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Publish(object message)
|
/// <inheritdoc />
|
||||||
|
public async Task Publish(object message)
|
||||||
{
|
{
|
||||||
return workerFactory().Publish(message, null, IsMandatory(message));
|
await Publish(message, null, IsMandatory(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Publish(object message, IBasicProperties properties, bool mandatory)
|
/// <inheritdoc />
|
||||||
|
public async Task Publish(object message, IMessageProperties properties, bool mandatory)
|
||||||
{
|
{
|
||||||
return workerFactory().Publish(message, properties, mandatory);
|
var messageClass = message.GetType();
|
||||||
|
var exchange = exchangeStrategy.GetExchange(messageClass);
|
||||||
|
var routingKey = routingKeyStrategy.GetRoutingKey(messageClass);
|
||||||
|
|
||||||
|
await Publish(message, properties, exchange, routingKey, mandatory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory)
|
/// <inheritdoc />
|
||||||
|
public async Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory)
|
||||||
{
|
{
|
||||||
return workerFactory().PublishDirect(message, queueName, properties, 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -39,5 +94,15 @@ namespace Tapeti.Connection
|
|||||||
{
|
{
|
||||||
return message.GetType().GetCustomAttribute<MandatoryAttribute>() != null;
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,39 +6,273 @@ using Tapeti.Config;
|
|||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public class TapetiSubscriber : ISubscriber
|
public class TapetiSubscriber : ISubscriber
|
||||||
{
|
{
|
||||||
private readonly Func<TapetiWorker> workerFactory;
|
private readonly Func<ITapetiClient> clientFactory;
|
||||||
private readonly List<IQueue> queues;
|
private readonly ITapetiConfig config;
|
||||||
private bool consuming;
|
private bool consuming;
|
||||||
|
|
||||||
|
|
||||||
public TapetiSubscriber(Func<TapetiWorker> workerFactory, IEnumerable<IQueue> queues)
|
/// <inheritdoc />
|
||||||
|
public TapetiSubscriber(Func<ITapetiClient> clientFactory, ITapetiConfig config)
|
||||||
{
|
{
|
||||||
this.workerFactory = workerFactory;
|
this.clientFactory = clientFactory;
|
||||||
this.queues = queues.ToList();
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task BindQueues()
|
/// <summary>
|
||||||
|
/// Applies the configured bindings and declares the queues in RabbitMQ. For internal use only.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task ApplyBindings()
|
||||||
{
|
{
|
||||||
return Task.WhenAll(queues.Select(queue => workerFactory().Subscribe(queue)).ToList());
|
var routingKeyStrategy = config.DependencyResolver.Resolve<IRoutingKeyStrategy>();
|
||||||
|
var exchangeStrategy = config.DependencyResolver.Resolve<IExchangeStrategy>();
|
||||||
|
|
||||||
|
var bindingTarget = config.Features.DeclareDurableQueues
|
||||||
|
? (CustomBindingTarget)new DeclareDurableQueuesBindingTarget(clientFactory, routingKeyStrategy, exchangeStrategy)
|
||||||
|
: new PassiveDurableQueuesBindingTarget(clientFactory, routingKeyStrategy, exchangeStrategy);
|
||||||
|
|
||||||
|
await Task.WhenAll(config.Bindings.Select(binding => binding.Apply(bindingTarget)));
|
||||||
|
await bindingTarget.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task RebindQueues()
|
/// <inheritdoc />
|
||||||
{
|
public async Task Resume()
|
||||||
return BindQueues();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task Resume()
|
|
||||||
{
|
{
|
||||||
if (consuming)
|
if (consuming)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
consuming = true;
|
consuming = true;
|
||||||
return Task.WhenAll(queues.Select(queue => workerFactory().Consume(queue.Name, queue.Bindings)).ToList());
|
|
||||||
|
var queues = config.Bindings.GroupBy(binding => binding.QueueName);
|
||||||
|
|
||||||
|
await Task.WhenAll(queues.Select(async group =>
|
||||||
|
{
|
||||||
|
var queueName = group.Key;
|
||||||
|
var consumer = new TapetiConsumer(config, queueName, group);
|
||||||
|
|
||||||
|
await clientFactory().Consume(queueName, consumer);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task ApplyBinding(IBinding binding, IBindingTarget bindingTarget)
|
||||||
|
{
|
||||||
|
await binding.Apply(bindingTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private abstract class CustomBindingTarget : IBindingTarget
|
||||||
|
{
|
||||||
|
protected readonly Func<ITapetiClient> ClientFactory;
|
||||||
|
protected readonly IRoutingKeyStrategy RoutingKeyStrategy;
|
||||||
|
protected readonly IExchangeStrategy ExchangeStrategy;
|
||||||
|
|
||||||
|
private struct DynamicQueueInfo
|
||||||
|
{
|
||||||
|
public string QueueName;
|
||||||
|
public List<Type> MessageClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new Dictionary<string, List<DynamicQueueInfo>>();
|
||||||
|
|
||||||
|
|
||||||
|
protected CustomBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy)
|
||||||
|
{
|
||||||
|
ClientFactory = clientFactory;
|
||||||
|
RoutingKeyStrategy = routingKeyStrategy;
|
||||||
|
ExchangeStrategy = exchangeStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual Task Apply()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public abstract Task BindDurable(Type messageClass, string queueName);
|
||||||
|
public abstract Task BindDirectDurable(string queueName);
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<string> BindDynamic(Type messageClass, string queuePrefix = null)
|
||||||
|
{
|
||||||
|
var result = await DeclareDynamicQueue(messageClass, queuePrefix);
|
||||||
|
|
||||||
|
if (result.IsNewMessageClass)
|
||||||
|
{
|
||||||
|
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
|
||||||
|
var exchange = ExchangeStrategy.GetExchange(messageClass);
|
||||||
|
|
||||||
|
await ClientFactory().DynamicQueueBind(result.QueueName, new QueueBinding(exchange, routingKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.QueueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<string> BindDirectDynamic(Type messageClass, string queuePrefix = null)
|
||||||
|
{
|
||||||
|
var result = await DeclareDynamicQueue(messageClass, queuePrefix);
|
||||||
|
return result.QueueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<string> BindDirectDynamic(string queuePrefix = null)
|
||||||
|
{
|
||||||
|
// If we don't know the routing key, always create a new queue to ensure there is no overlap.
|
||||||
|
// Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either.
|
||||||
|
return await ClientFactory().DynamicQueueDeclare(queuePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private struct DeclareDynamicQueueResult
|
||||||
|
{
|
||||||
|
public string QueueName;
|
||||||
|
public bool IsNewMessageClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix)
|
||||||
|
{
|
||||||
|
// Group by prefix
|
||||||
|
var key = queuePrefix ?? "";
|
||||||
|
if (!dynamicQueues.TryGetValue(key, out var prefixQueues))
|
||||||
|
{
|
||||||
|
prefixQueues = new List<DynamicQueueInfo>();
|
||||||
|
dynamicQueues.Add(key, prefixQueues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure routing keys are unique per dynamic queue, so that a requeue
|
||||||
|
// will not cause the side-effect of calling another handler again as well.
|
||||||
|
foreach (var existingQueueInfo in prefixQueues)
|
||||||
|
{
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (!existingQueueInfo.MessageClasses.Contains(messageClass))
|
||||||
|
{
|
||||||
|
// Allow this routing key in the existing dynamic queue
|
||||||
|
var result = new DeclareDynamicQueueResult
|
||||||
|
{
|
||||||
|
QueueName = existingQueueInfo.QueueName,
|
||||||
|
IsNewMessageClass = !existingQueueInfo.MessageClasses.Contains(messageClass)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.IsNewMessageClass)
|
||||||
|
existingQueueInfo.MessageClasses.Add(messageClass);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare a new queue
|
||||||
|
var queueName = await ClientFactory().DynamicQueueDeclare(queuePrefix);
|
||||||
|
var queueInfo = new DynamicQueueInfo
|
||||||
|
{
|
||||||
|
QueueName = queueName,
|
||||||
|
MessageClasses = new List<Type> { messageClass }
|
||||||
|
};
|
||||||
|
|
||||||
|
prefixQueues.Add(queueInfo);
|
||||||
|
|
||||||
|
return new DeclareDynamicQueueResult
|
||||||
|
{
|
||||||
|
QueueName = queueName,
|
||||||
|
IsNewMessageClass = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class DeclareDurableQueuesBindingTarget : CustomBindingTarget
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, List<Type>> durableQueues = new Dictionary<string, List<Type>>();
|
||||||
|
|
||||||
|
|
||||||
|
public DeclareDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy) : base(clientFactory, routingKeyStrategy, exchangeStrategy)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override Task BindDurable(Type messageClass, string queueName)
|
||||||
|
{
|
||||||
|
// Collect the message classes per queue so we can determine afterwards
|
||||||
|
// if any of the bindings currently set on the durable queue are no
|
||||||
|
// longer valid and should be removed.
|
||||||
|
if (!durableQueues.TryGetValue(queueName, out var messageClasses))
|
||||||
|
{
|
||||||
|
durableQueues.Add(queueName, new List<Type>
|
||||||
|
{
|
||||||
|
messageClass
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!messageClasses.Contains(messageClass))
|
||||||
|
messageClasses.Add(messageClass);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override Task BindDirectDurable(string queueName)
|
||||||
|
{
|
||||||
|
if (!durableQueues.ContainsKey(queueName))
|
||||||
|
durableQueues.Add(queueName, new List<Type>());
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task Apply()
|
||||||
|
{
|
||||||
|
var worker = ClientFactory();
|
||||||
|
|
||||||
|
await Task.WhenAll(durableQueues.Select(async queue =>
|
||||||
|
{
|
||||||
|
var bindings = queue.Value.Select(messageClass =>
|
||||||
|
{
|
||||||
|
var exchange = ExchangeStrategy.GetExchange(messageClass);
|
||||||
|
var routingKey = RoutingKeyStrategy.GetRoutingKey(messageClass);
|
||||||
|
|
||||||
|
return new QueueBinding(exchange, routingKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
await worker.DurableQueueDeclare(queue.Key, bindings);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class PassiveDurableQueuesBindingTarget : CustomBindingTarget
|
||||||
|
{
|
||||||
|
private readonly List<string> durableQueues = new List<string>();
|
||||||
|
|
||||||
|
|
||||||
|
public PassiveDurableQueuesBindingTarget(Func<ITapetiClient> clientFactory, IRoutingKeyStrategy routingKeyStrategy, IExchangeStrategy exchangeStrategy) : base(clientFactory, routingKeyStrategy, exchangeStrategy)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task BindDurable(Type messageClass, string queueName)
|
||||||
|
{
|
||||||
|
await VerifyDurableQueue(queueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task BindDirectDurable(string queueName)
|
||||||
|
{
|
||||||
|
await VerifyDurableQueue(queueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task VerifyDurableQueue(string queueName)
|
||||||
|
{
|
||||||
|
if (!durableQueues.Contains(queueName))
|
||||||
|
{
|
||||||
|
await ClientFactory().DurableQueueVerify(queueName);
|
||||||
|
durableQueues.Add(queueName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,23 @@
|
|||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the response sent back after handling a message.
|
||||||
|
/// </summary>
|
||||||
public enum ConsumeResponse
|
public enum ConsumeResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledge the message and remove it from the queue
|
||||||
|
/// </summary>
|
||||||
Ack,
|
Ack,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Negatively acknowledge the message and remove it from the queue, send to dead-letter queue if configured on the bus
|
||||||
|
/// </summary>
|
||||||
Nack,
|
Nack,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Negatively acknowledge the message and put it back in the queue to try again later
|
||||||
|
/// </summary>
|
||||||
Requeue
|
Requeue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
Tapeti/Default/ControllerMessageContext.cs
Normal file
50
Tapeti/Default/ControllerMessageContext.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="IControllerMessageContext" />
|
||||||
|
public class ControllerMessageContext : MessageContext, IControllerMessageContext
|
||||||
|
{
|
||||||
|
private Dictionary<string, object> items = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object Controller { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public new IControllerMethodBinding Binding { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var item in items.Values)
|
||||||
|
(item as IDisposable)?.Dispose();
|
||||||
|
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Store(string key, object value)
|
||||||
|
{
|
||||||
|
items.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Get<T>(string key, out T value) where T : class
|
||||||
|
{
|
||||||
|
if (!items.TryGetValue(key, out var objectValue))
|
||||||
|
{
|
||||||
|
value = default(T);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = (T)objectValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
Tapeti/Default/ControllerMethodBinding.cs
Normal file
77
Tapeti/Default/ControllerMethodBinding.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Binding implementation for controller methods. Do not instantiate this class yourself,
|
||||||
|
/// instead use the ITapetiConfigBuilder RegisterController / RegisterAllControllers extension
|
||||||
|
/// methods.
|
||||||
|
/// </summary>
|
||||||
|
public class ControllerMethodBinding : IBinding
|
||||||
|
{
|
||||||
|
private readonly Type controller;
|
||||||
|
private readonly MethodInfo method;
|
||||||
|
private readonly QueueInfo queueInfo;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string QueueName { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ControllerMethodBinding(Type controller, MethodInfo method, QueueInfo queueInfo)
|
||||||
|
{
|
||||||
|
this.controller = controller;
|
||||||
|
this.method = method;
|
||||||
|
this.queueInfo = queueInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task Apply(IBindingTarget target)
|
||||||
|
{
|
||||||
|
// TODO ControllerMethodBinding
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Accept(Type messageClass)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task Invoke(IMessageContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class QueueInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the queue is dynamic or durable.
|
||||||
|
/// </summary>
|
||||||
|
public bool Dynamic { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the durable queue, or optional prefix of the dynamic queue.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the QueueInfo properties contain a valid combination.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsValid => Dynamic|| !string.IsNullOrEmpty(Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,14 +4,20 @@ using Tapeti.Config;
|
|||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class DependencyResolverBinding : IBindingMiddleware
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to resolve any unhandled parameters to Controller methods using the IoC container.
|
||||||
|
/// This middleware is included by default in the standard TapetiConfig.
|
||||||
|
/// </summary>
|
||||||
|
public class DependencyResolverBinding : IControllerBindingMiddleware
|
||||||
{
|
{
|
||||||
public void Handle(IBindingContext context, Action next)
|
/// <inheritdoc />
|
||||||
|
public void Handle(IControllerBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
next();
|
next();
|
||||||
|
|
||||||
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType.IsClass))
|
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType.IsClass))
|
||||||
parameter.SetBinding(messageContext => messageContext.DependencyResolver.Resolve(parameter.Info.ParameterType));
|
parameter.SetBinding(messageContext => messageContext.Config.DependencyResolver.Resolve(parameter.Info.ParameterType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts an <see cref="Enum"/> to and from its name string value. If an unknown string value is encountered
|
/// Converts an <see cref="T:System.Enum" /> to and from its name string value. If an unknown string value is encountered
|
||||||
/// it will translate to 0xDEADBEEF (-559038737) so it can be gracefully handled.
|
/// it will translate to 0xDEADBEEF (-559038737) so it can be gracefully handled.
|
||||||
/// If you copy this value as-is to another message and try to send it, this converter will throw an exception.
|
/// If you copy this value as-is to another message and try to send it, this converter will throw an exception.
|
||||||
///
|
|
||||||
/// This converter is far simpler than the default StringEnumConverter, it assumes both sides use the same
|
/// This converter is far simpler than the default StringEnumConverter, it assumes both sides use the same
|
||||||
/// enum and therefore skips the naming strategy.
|
/// enum and therefore skips the naming strategy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -17,12 +17,14 @@ namespace Tapeti.Default
|
|||||||
private readonly int invalidEnumValue;
|
private readonly int invalidEnumValue;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public FallbackStringEnumConverter()
|
public FallbackStringEnumConverter()
|
||||||
{
|
{
|
||||||
unchecked { invalidEnumValue = (int)0xDEADBEEF; }
|
unchecked { invalidEnumValue = (int)0xDEADBEEF; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
@ -39,6 +41,7 @@ namespace Tapeti.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var isNullable = IsNullableType(objectType);
|
var isNullable = IsNullableType(objectType);
|
||||||
@ -72,6 +75,7 @@ namespace Tapeti.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type objectType)
|
public override bool CanConvert(Type objectType)
|
||||||
{
|
{
|
||||||
var actualType = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;
|
var actualType = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RabbitMQ.Client;
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// IMessageSerializer implementation for JSON encoding and decoding using Newtonsoft.Json.
|
||||||
|
/// </summary>
|
||||||
public class JsonMessageSerializer : IMessageSerializer
|
public class JsonMessageSerializer : IMessageSerializer
|
||||||
{
|
{
|
||||||
protected const string ContentType = "application/json";
|
private const string ContentType = "application/json";
|
||||||
protected const string ClassTypeHeader = "classType";
|
private const string ClassTypeHeader = "classType";
|
||||||
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>();
|
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>();
|
||||||
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
||||||
private readonly JsonSerializerSettings serializerSettings;
|
private readonly JsonSerializerSettings serializerSettings;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public JsonMessageSerializer()
|
public JsonMessageSerializer()
|
||||||
{
|
{
|
||||||
serializerSettings = new JsonSerializerSettings
|
serializerSettings = new JsonSerializerSettings
|
||||||
@ -28,35 +33,41 @@ namespace Tapeti.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public byte[] Serialize(object message, IBasicProperties properties)
|
/// <inheritdoc />
|
||||||
|
public byte[] Serialize(object message, IMessageProperties properties)
|
||||||
{
|
{
|
||||||
if (properties.Headers == null)
|
|
||||||
properties.Headers = new Dictionary<string, object>();
|
|
||||||
|
|
||||||
var typeName = serializedTypeNames.GetOrAdd(message.GetType(), SerializeTypeName);
|
var typeName = serializedTypeNames.GetOrAdd(message.GetType(), SerializeTypeName);
|
||||||
|
|
||||||
properties.Headers.Add(ClassTypeHeader, Encoding.UTF8.GetBytes(typeName));
|
properties.SetHeader(ClassTypeHeader, typeName);
|
||||||
properties.ContentType = ContentType;
|
properties.ContentType = ContentType;
|
||||||
|
|
||||||
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message, serializerSettings));
|
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message, serializerSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public object Deserialize(byte[] body, IBasicProperties properties)
|
/// <inheritdoc />
|
||||||
|
public object Deserialize(byte[] body, IMessageProperties properties)
|
||||||
{
|
{
|
||||||
if (properties.ContentType == null || !properties.ContentType.Equals(ContentType))
|
if (properties.ContentType == null || !properties.ContentType.Equals(ContentType))
|
||||||
throw new ArgumentException($"content_type must be {ContentType}");
|
throw new ArgumentException($"content_type must be {ContentType}");
|
||||||
|
|
||||||
if (properties.Headers == null || !properties.Headers.TryGetValue(ClassTypeHeader, out var typeName))
|
var typeName = properties.GetHeader(ClassTypeHeader);
|
||||||
|
if (string.IsNullOrEmpty(typeName))
|
||||||
throw new ArgumentException($"{ClassTypeHeader} header not present");
|
throw new ArgumentException($"{ClassTypeHeader} header not present");
|
||||||
|
|
||||||
var messageType = deserializedTypeNames.GetOrAdd(Encoding.UTF8.GetString((byte[])typeName), DeserializeTypeName);
|
var messageType = deserializedTypeNames.GetOrAdd(typeName, DeserializeTypeName);
|
||||||
return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), messageType, serializerSettings);
|
return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), messageType, serializerSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public virtual Type DeserializeTypeName(string typeName)
|
/// <summary>
|
||||||
|
/// Resolves a Type based on the serialized type name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeName">The type name in the format FullNamespace.ClassName:AssemblyName</param>
|
||||||
|
/// <returns>The resolved Type</returns>
|
||||||
|
/// <exception cref="ArgumentException">If the format is unrecognized or the Type could not be resolved</exception>
|
||||||
|
protected virtual Type DeserializeTypeName(string typeName)
|
||||||
{
|
{
|
||||||
var parts = typeName.Split(':');
|
var parts = typeName.Split(':');
|
||||||
if (parts.Length != 2)
|
if (parts.Length != 2)
|
||||||
@ -69,7 +80,14 @@ namespace Tapeti.Default
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual string SerializeTypeName(Type type)
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes a Type into a string representation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to serialize</param>
|
||||||
|
/// <returns>The type name in the format FullNamespace.ClassName:AssemblyName</returns>
|
||||||
|
/// <exception cref="ArgumentException">If the serialized type name results in the AMQP limit of 255 characters to be exceeded</exception>
|
||||||
|
protected virtual string SerializeTypeName(Type type)
|
||||||
{
|
{
|
||||||
var typeName = type.FullName + ":" + type.Assembly.GetName().Name;
|
var typeName = type.FullName + ":" + type.Assembly.GetName().Name;
|
||||||
if (typeName.Length > 255)
|
if (typeName.Length > 255)
|
||||||
|
@ -3,9 +3,15 @@ using Tapeti.Config;
|
|||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class MessageBinding : IBindingMiddleware
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message class from the first parameter of a controller method.
|
||||||
|
/// This middleware is included by default in the standard TapetiConfig.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageBinding : IControllerBindingMiddleware
|
||||||
{
|
{
|
||||||
public void Handle(IBindingContext context, Action next)
|
/// <inheritdoc />
|
||||||
|
public void Handle(IControllerBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
if (context.Parameters.Count == 0)
|
if (context.Parameters.Count == 0)
|
||||||
throw new TopologyConfigurationException($"First parameter of method {context.Method.Name} in controller {context.Method.DeclaringType?.Name} must be a message class");
|
throw new TopologyConfigurationException($"First parameter of method {context.Method.Name} in controller {context.Method.DeclaringType?.Name} must be a message class");
|
||||||
@ -15,7 +21,7 @@ namespace Tapeti.Default
|
|||||||
throw new TopologyConfigurationException($"First parameter {parameter.Info.Name} of method {context.Method.Name} in controller {context.Method.DeclaringType?.Name} must be a message class");
|
throw new TopologyConfigurationException($"First parameter {parameter.Info.Name} of method {context.Method.Name} in controller {context.Method.DeclaringType?.Name} must be a message class");
|
||||||
|
|
||||||
parameter.SetBinding(messageContext => messageContext.Message);
|
parameter.SetBinding(messageContext => messageContext.Message);
|
||||||
context.MessageClass = parameter.Info.ParameterType;
|
context.SetMessageClass(parameter.Info.ParameterType);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
@ -1,172 +1,34 @@
|
|||||||
using System;
|
using Tapeti.Config;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using RabbitMQ.Client;
|
|
||||||
using Tapeti.Config;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public class MessageContext : IMessageContext
|
public class MessageContext : IMessageContext
|
||||||
{
|
{
|
||||||
public IDependencyResolver DependencyResolver { get; set; }
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfig Config { get; set; }
|
||||||
|
|
||||||
public object Controller { get; set; }
|
/// <inheritdoc />
|
||||||
|
public string Queue { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Exchange { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string RoutingKey { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object Message { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IMessageProperties Properties { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IBinding Binding { get; set; }
|
public IBinding Binding { get; set; }
|
||||||
|
|
||||||
public string Queue { get; set; }
|
/// <inheritdoc />
|
||||||
public string RoutingKey { get; set; }
|
public virtual void Dispose()
|
||||||
public object Message { get; set; }
|
|
||||||
public IBasicProperties Properties { get; set; }
|
|
||||||
|
|
||||||
public IDictionary<string, object> Items { get; }
|
|
||||||
|
|
||||||
internal Action<MessageContext> UseNestedContext;
|
|
||||||
internal Action<MessageContext> OnContextDisposed;
|
|
||||||
|
|
||||||
public MessageContext()
|
|
||||||
{
|
{
|
||||||
Items = new Dictionary<string, object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageContext(MessageContext outerContext)
|
|
||||||
{
|
|
||||||
DependencyResolver = outerContext.DependencyResolver;
|
|
||||||
|
|
||||||
Controller = outerContext.Controller;
|
|
||||||
Binding = outerContext.Binding;
|
|
||||||
|
|
||||||
Queue = outerContext.Queue;
|
|
||||||
RoutingKey = outerContext.RoutingKey;
|
|
||||||
Message = outerContext.Message;
|
|
||||||
Properties = outerContext.Properties;
|
|
||||||
|
|
||||||
Items = new DeferingDictionary(outerContext.Items);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
var items = (Items as DeferingDictionary)?.MyState ?? Items;
|
|
||||||
|
|
||||||
foreach (var value in items.Values)
|
|
||||||
(value as IDisposable)?.Dispose();
|
|
||||||
|
|
||||||
OnContextDisposed?.Invoke(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IMessageContext SetupNestedContext()
|
|
||||||
{
|
|
||||||
if (UseNestedContext == null)
|
|
||||||
throw new NotSupportedException("This context does not support creating nested contexts");
|
|
||||||
|
|
||||||
var nested = new MessageContext(this);
|
|
||||||
|
|
||||||
UseNestedContext(nested);
|
|
||||||
|
|
||||||
return nested;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DeferingDictionary : IDictionary<string, object>
|
|
||||||
{
|
|
||||||
private readonly IDictionary<string, object> myState;
|
|
||||||
private readonly IDictionary<string, object> deferee;
|
|
||||||
|
|
||||||
public DeferingDictionary(IDictionary<string, object> deferee)
|
|
||||||
{
|
|
||||||
myState = new Dictionary<string, object>();
|
|
||||||
this.deferee = deferee;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, object> MyState => myState;
|
|
||||||
|
|
||||||
object IDictionary<string, object>.this[string key]
|
|
||||||
{
|
|
||||||
get => myState.ContainsKey(key) ? myState[key] : deferee[key];
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (deferee.ContainsKey(key))
|
|
||||||
throw new InvalidOperationException("Cannot hide an item set in an outer context.");
|
|
||||||
|
|
||||||
myState[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int ICollection<KeyValuePair<string, object>>.Count => myState.Count + deferee.Count;
|
|
||||||
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
|
|
||||||
ICollection<string> IDictionary<string, object>.Keys => myState.Keys.Concat(deferee.Keys).ToList().AsReadOnly();
|
|
||||||
ICollection<object> IDictionary<string, object>.Values => myState.Values.Concat(deferee.Values).ToList().AsReadOnly();
|
|
||||||
|
|
||||||
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
|
||||||
{
|
|
||||||
if (deferee.ContainsKey(item.Key))
|
|
||||||
throw new InvalidOperationException("Cannot hide an item set in an outer context.");
|
|
||||||
|
|
||||||
myState.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IDictionary<string, object>.Add(string key, object value)
|
|
||||||
{
|
|
||||||
if (deferee.ContainsKey(key))
|
|
||||||
throw new InvalidOperationException("Cannot hide an item set in an outer context.");
|
|
||||||
|
|
||||||
myState.Add(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ICollection<KeyValuePair<string, object>>.Clear()
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Cannot influence the items in an outer context.");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
|
|
||||||
{
|
|
||||||
return myState.Contains(item) || deferee.Contains(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IDictionary<string, object>.ContainsKey(string key)
|
|
||||||
{
|
|
||||||
return myState.ContainsKey(key) || deferee.ContainsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
|
|
||||||
{
|
|
||||||
foreach(var item in myState.Concat(deferee))
|
|
||||||
{
|
|
||||||
array[arrayIndex++] = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return (IEnumerator)myState.Concat(deferee);
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
|
||||||
{
|
|
||||||
return (IEnumerator < KeyValuePair < string, object>> )myState.Concat(deferee);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
|
|
||||||
{
|
|
||||||
if (deferee.ContainsKey(item.Key))
|
|
||||||
throw new InvalidOperationException("Cannot remove an item set in an outer context.");
|
|
||||||
|
|
||||||
return myState.Remove(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IDictionary<string, object>.Remove(string key)
|
|
||||||
{
|
|
||||||
if (deferee.ContainsKey(key))
|
|
||||||
throw new InvalidOperationException("Cannot remove an item set in an outer context.");
|
|
||||||
|
|
||||||
return myState.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IDictionary<string, object>.TryGetValue(string key, out object value)
|
|
||||||
{
|
|
||||||
return myState.TryGetValue(key, out value)
|
|
||||||
|| deferee.TryGetValue(key, out value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
77
Tapeti/Default/MessageProperties.cs
Normal file
77
Tapeti/Default/MessageProperties.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// IMessagePropertiesReader implementation for providing properties manually
|
||||||
|
/// </summary>
|
||||||
|
public class MessageProperties : IMessageProperties
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string> headers = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ReplyTo { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool? Persistent { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? Timestamp { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MessageProperties()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MessageProperties(IMessageProperties source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ContentType = source.ContentType;
|
||||||
|
CorrelationId = source.CorrelationId;
|
||||||
|
ReplyTo = source.ReplyTo;
|
||||||
|
Persistent = source.Persistent;
|
||||||
|
Timestamp = source.Timestamp;
|
||||||
|
|
||||||
|
headers.Clear();
|
||||||
|
foreach (var pair in source.GetHeaders())
|
||||||
|
SetHeader(pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetHeader(string name, string value)
|
||||||
|
{
|
||||||
|
if (headers.ContainsKey(name))
|
||||||
|
headers[name] = value;
|
||||||
|
else
|
||||||
|
headers.Add(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetHeader(string name)
|
||||||
|
{
|
||||||
|
return headers.TryGetValue(name, out var value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<KeyValuePair<string, string>> GetHeaders()
|
||||||
|
{
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,16 +2,20 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client.Framing;
|
|
||||||
using Tapeti.Annotations;
|
using Tapeti.Annotations;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Helpers;
|
using Tapeti.Helpers;
|
||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class PublishResultBinding : IBindingMiddleware
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to publish a return value for Controller methods as a response to the incoming message.
|
||||||
|
/// </summary>
|
||||||
|
public class PublishResultBinding : IControllerBindingMiddleware
|
||||||
{
|
{
|
||||||
public void Handle(IBindingContext context, Action next)
|
/// <inheritdoc />
|
||||||
|
public void Handle(IControllerBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
next();
|
next();
|
||||||
|
|
||||||
@ -60,18 +64,15 @@ namespace Tapeti.Default
|
|||||||
if (message == null)
|
if (message == null)
|
||||||
throw new ArgumentException("Return value of a request message handler must not be null");
|
throw new ArgumentException("Return value of a request message handler must not be null");
|
||||||
|
|
||||||
var publisher = (IInternalPublisher)messageContext.DependencyResolver.Resolve<IPublisher>();
|
var publisher = (IInternalPublisher)messageContext.Config.DependencyResolver.Resolve<IPublisher>();
|
||||||
var properties = new BasicProperties();
|
var properties = new MessageProperties
|
||||||
|
{
|
||||||
|
CorrelationId = messageContext.Properties.CorrelationId
|
||||||
|
};
|
||||||
|
|
||||||
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
return !string.IsNullOrEmpty(messageContext.Properties.ReplyTo)
|
||||||
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
? publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties, messageContext.Properties.Persistent.GetValueOrDefault(true))
|
||||||
if (messageContext.Properties.IsCorrelationIdPresent())
|
: publisher.Publish(message, properties, false);
|
||||||
properties.CorrelationId = messageContext.Properties.CorrelationId;
|
|
||||||
|
|
||||||
if (messageContext.Properties.IsReplyToPresent())
|
|
||||||
return publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties, messageContext.Properties.Persistent);
|
|
||||||
|
|
||||||
return publisher.Publish(message, properties, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
119
Tapeti/Default/RabbitMQMessageProperties.cs
Normal file
119
Tapeti/Default/RabbitMQMessageProperties.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Wrapper for RabbitMQ Client's IBasicProperties
|
||||||
|
/// </summary>
|
||||||
|
public class RabbitMQMessageProperties : IMessageProperties
|
||||||
|
{
|
||||||
|
public IBasicProperties BasicProperties { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ContentType
|
||||||
|
{
|
||||||
|
get => BasicProperties.IsContentTypePresent() ? BasicProperties.ContentType : null;
|
||||||
|
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ContentType = value; else BasicProperties.ClearContentType(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string CorrelationId
|
||||||
|
{
|
||||||
|
get => BasicProperties.IsCorrelationIdPresent() ? BasicProperties.CorrelationId : null;
|
||||||
|
set { if (!string.IsNullOrEmpty(value)) BasicProperties.CorrelationId = value; else BasicProperties.ClearCorrelationId(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ReplyTo
|
||||||
|
{
|
||||||
|
get => BasicProperties.IsReplyToPresent() ? BasicProperties.ReplyTo : null;
|
||||||
|
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ReplyTo = value; else BasicProperties.ClearReplyTo(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool? Persistent
|
||||||
|
{
|
||||||
|
get => BasicProperties.Persistent;
|
||||||
|
set { if (value.HasValue) BasicProperties.Persistent = value.Value; else BasicProperties.ClearDeliveryMode(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? Timestamp
|
||||||
|
{
|
||||||
|
get => DateTimeOffset.FromUnixTimeSeconds(BasicProperties.Timestamp.UnixTime).UtcDateTime;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.HasValue)
|
||||||
|
BasicProperties.Timestamp = new AmqpTimestamp(new DateTimeOffset(value.Value.ToUniversalTime()).ToUnixTimeSeconds());
|
||||||
|
else
|
||||||
|
BasicProperties.ClearTimestamp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public RabbitMQMessageProperties(IBasicProperties BasicProperties)
|
||||||
|
{
|
||||||
|
this.BasicProperties = BasicProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public RabbitMQMessageProperties(IBasicProperties BasicProperties, IMessageProperties source)
|
||||||
|
{
|
||||||
|
this.BasicProperties = BasicProperties;
|
||||||
|
if (source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ContentType = source.ContentType;
|
||||||
|
CorrelationId = source.CorrelationId;
|
||||||
|
ReplyTo = source.ReplyTo;
|
||||||
|
Persistent = source.Persistent;
|
||||||
|
Timestamp = source.Timestamp;
|
||||||
|
|
||||||
|
BasicProperties.Headers = null;
|
||||||
|
foreach (var pair in source.GetHeaders())
|
||||||
|
SetHeader(pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetHeader(string name, string value)
|
||||||
|
{
|
||||||
|
if (BasicProperties.Headers == null)
|
||||||
|
BasicProperties.Headers = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
if (BasicProperties.Headers.ContainsKey(name))
|
||||||
|
BasicProperties.Headers[name] = Encoding.UTF8.GetBytes(value);
|
||||||
|
else
|
||||||
|
BasicProperties.Headers.Add(name, Encoding.UTF8.GetBytes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetHeader(string name)
|
||||||
|
{
|
||||||
|
if (BasicProperties.Headers == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return BasicProperties.Headers.TryGetValue(name, out var value) ? Encoding.UTF8.GetString((byte[])value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<KeyValuePair<string, string>> GetHeaders()
|
||||||
|
{
|
||||||
|
if (BasicProperties.Headers == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (var pair in BasicProperties.Headers)
|
||||||
|
yield return new KeyValuePair<string, string>(pair.Key, Encoding.UTF8.GetString((byte[])pair.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace Tapeti.Helpers
|
namespace Tapeti.Helpers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class to construct a TapetiConnectionParams instance based on the
|
||||||
|
/// ConnectionString syntax as used by EasyNetQ.
|
||||||
|
/// </summary>
|
||||||
public class ConnectionStringParser
|
public class ConnectionStringParser
|
||||||
{
|
{
|
||||||
private readonly TapetiConnectionParams result = new TapetiConnectionParams();
|
private readonly TapetiConnectionParams result = new TapetiConnectionParams();
|
||||||
@ -10,6 +14,10 @@ namespace Tapeti.Helpers
|
|||||||
private int pos = -1;
|
private int pos = -1;
|
||||||
private char current = '\0';
|
private char current = '\0';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses an EasyNetQ-compatible ConnectionString into a TapetiConnectionParams instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connectionstring"></param>
|
||||||
public static TapetiConnectionParams Parse(string connectionstring)
|
public static TapetiConnectionParams Parse(string connectionstring)
|
||||||
{
|
{
|
||||||
return new ConnectionStringParser(connectionstring).result;
|
return new ConnectionStringParser(connectionstring).result;
|
||||||
@ -106,7 +114,9 @@ namespace Tapeti.Helpers
|
|||||||
|
|
||||||
private void SetValue(string key, string value)
|
private void SetValue(string key, string value)
|
||||||
{
|
{
|
||||||
switch (key.ToLowerInvariant()) {
|
// ReSharper disable once SwitchStatementMissingSomeCases - by design, don't fail on unknown properties
|
||||||
|
switch (key.ToLowerInvariant())
|
||||||
|
{
|
||||||
case "hostname": result.HostName = value; break;
|
case "hostname": result.HostName = value; break;
|
||||||
case "port": result.Port = int.Parse(value); break;
|
case "port": result.Port = int.Parse(value); break;
|
||||||
case "virtualhost": result.VirtualHost = value; break;
|
case "virtualhost": result.VirtualHost = value; break;
|
||||||
|
@ -5,8 +5,76 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information about the reason for a lost connection.
|
||||||
|
/// </summary>
|
||||||
|
public class DisconnectedEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ReplyCode as indicated by the client library
|
||||||
|
/// </summary>
|
||||||
|
public ushort ReplyCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ReplyText as indicated by the client library
|
||||||
|
/// </summary>
|
||||||
|
public string ReplyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public delegate void DisconnectedEventHandler(object sender, DisconnectedEventArgs e);
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a connection to a RabbitMQ server
|
||||||
|
/// </summary>
|
||||||
public interface IConnection : IDisposable
|
public interface IConnection : IDisposable
|
||||||
{
|
{
|
||||||
Task<ISubscriber> Subscribe();
|
/// <summary>
|
||||||
|
/// Creates a subscriber to consume messages from the bound queues.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startConsuming">If true, the subscriber will start consuming messages immediately. If false, the queues will be
|
||||||
|
/// declared but no messages will be consumed yet. Call Resume on the returned ISubscriber to start consuming messages.</param>
|
||||||
|
Task<ISubscriber> Subscribe(bool startConsuming = true);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronous version of Subscribe.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startConsuming">If true, the subscriber will start consuming messages immediately. If false, the queues will be
|
||||||
|
/// declared but no messages will be consumed yet. Call Resume on the returned ISubscriber to start consuming messages.</param>
|
||||||
|
ISubscriber SubscribeSync(bool startConsuming = true);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an IPublisher implementation for the current connection.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IPublisher GetPublisher();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the connection to RabbitMQ.
|
||||||
|
/// </summary>
|
||||||
|
Task Close();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when a connection to RabbitMQ has been established.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler Connected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the connection to RabbitMQ has been lost.
|
||||||
|
/// </summary>
|
||||||
|
event DisconnectedEventHandler Disconnected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the connection to RabbitMQ has been recovered after an unexpected disconnect.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler Reconnected;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
Tapeti/IConsumer.cs
Normal file
21
Tapeti/IConsumer.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Processes incoming messages.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConsumer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exchange">The exchange from which the message originated</param>
|
||||||
|
/// <param name="routingKey">The routing key the message was sent with</param>
|
||||||
|
/// <param name="properties">Metadata included in the message</param>
|
||||||
|
/// <param name="body">The raw body of the message</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ConsumeResponse> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wrapper interface for an IoC container to allow dependency injection in Tapeti.
|
||||||
|
/// </summary>
|
||||||
public interface IDependencyResolver
|
public interface IDependencyResolver
|
||||||
{
|
{
|
||||||
T Resolve<T>() where T : class;
|
T Resolve<T>() where T : class;
|
||||||
@ -9,6 +12,10 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows registering controller classes into the IoC container. Also registers default implementations,
|
||||||
|
/// so that the calling application may override these.
|
||||||
|
/// </summary>
|
||||||
public interface IDependencyContainer : IDependencyResolver
|
public interface IDependencyContainer : IDependencyResolver
|
||||||
{
|
{
|
||||||
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||||
|
@ -1,10 +1,26 @@
|
|||||||
using RabbitMQ.Client;
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides serialization and deserialization for messages.
|
||||||
|
/// </summary>
|
||||||
public interface IMessageSerializer
|
public interface IMessageSerializer
|
||||||
{
|
{
|
||||||
byte[] Serialize(object message, IBasicProperties properties);
|
/// <summary>
|
||||||
object Deserialize(byte[] body, IBasicProperties properties);
|
/// Serialize a message object instance to a byte array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">An instance of a message class</param>
|
||||||
|
/// <param name="properties">Writable access to the message properties which will be sent along with the message</param>
|
||||||
|
/// <returns>The encoded message</returns>
|
||||||
|
byte[] Serialize(object message, IMessageProperties properties);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes a previously serialized message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="body">The encoded message</param>
|
||||||
|
/// <param name="properties">The properties as sent along with the message</param>
|
||||||
|
/// <returns>A decoded instance of the message</returns>
|
||||||
|
object Deserialize(byte[] body, IMessageProperties properties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,50 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client;
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedMember.Global
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
// Note: Tapeti assumes every implementation of IPublisher can also be cast to an IInternalPublisher.
|
/// <summary>
|
||||||
// The distinction is made on purpose to trigger code-smells in non-Tapeti code when casting.
|
/// Allows publishing of messages.
|
||||||
|
/// </summary>
|
||||||
public interface IPublisher
|
public interface IPublisher
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Publish the specified message. Transport details are determined by the Tapeti configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to send</param>
|
||||||
Task Publish(object message);
|
Task Publish(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
|
public interface IInternalPublisher : IPublisher
|
||||||
{
|
{
|
||||||
Task Publish(object message, IBasicProperties properties, bool mandatory);
|
/// <summary>
|
||||||
Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory);
|
/// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages subscriptions to queues as configured by the bindings.
|
||||||
|
/// </summary>
|
||||||
public interface ISubscriber
|
public interface ISubscriber
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts consuming from the subscribed queues if not already started.
|
||||||
|
/// </summary>
|
||||||
Task Resume();
|
Task Resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn>1701;1702</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||||
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" />
|
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" />
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Tapeti.Annotations;
|
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Default;
|
using Tapeti.Default;
|
||||||
using Tapeti.Helpers;
|
using Tapeti.Helpers;
|
||||||
@ -12,198 +10,169 @@ using Tapeti.Helpers;
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
public class TopologyConfigurationException : Exception
|
/// <inheritdoc cref="ITapetiConfigBuilder" />
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of the Tapeti config builder.
|
||||||
|
/// Automatically registers the default middleware for injecting the message parameter and handling the return value.
|
||||||
|
/// </summary>
|
||||||
|
public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess
|
||||||
{
|
{
|
||||||
public TopologyConfigurationException(string message) : base(message) { }
|
private Config config;
|
||||||
}
|
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>();
|
||||||
|
|
||||||
public delegate Task MessageHandlerFunc(IMessageContext context, object message);
|
|
||||||
|
|
||||||
|
|
||||||
public class TapetiConfig
|
/// <inheritdoc />
|
||||||
{
|
public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver;
|
||||||
private readonly Dictionary<string, List<IBinding>> staticRegistrations = new Dictionary<string, List<IBinding>>();
|
|
||||||
private readonly Dictionary<string, Dictionary<Type, List<IBinding>>> dynamicRegistrations = new Dictionary<string, Dictionary<Type, List<IBinding>>>();
|
|
||||||
private readonly List<IBindingQueueInfo> uniqueRegistrations = new List<IBindingQueueInfo>();
|
|
||||||
|
|
||||||
private readonly List<ICustomBinding> customBindings = new List<ICustomBinding>();
|
|
||||||
private readonly List<IBindingMiddleware> bindingMiddleware = new List<IBindingMiddleware>();
|
|
||||||
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
|
|
||||||
private readonly List<ICleanupMiddleware> cleanupMiddleware = new List<ICleanupMiddleware>();
|
|
||||||
private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>();
|
|
||||||
|
|
||||||
private readonly IDependencyResolver dependencyResolver;
|
|
||||||
|
|
||||||
private bool usePublisherConfirms = true;
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new Tapeti config builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dependencyResolver">A wrapper implementation for an IoC container to allow dependency injection</param>
|
||||||
public TapetiConfig(IDependencyResolver dependencyResolver)
|
public TapetiConfig(IDependencyResolver dependencyResolver)
|
||||||
{
|
{
|
||||||
this.dependencyResolver = dependencyResolver;
|
config = new Config(dependencyResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfig Build()
|
||||||
|
{
|
||||||
|
if (config == null)
|
||||||
|
throw new InvalidOperationException("TapetiConfig.Build must only be called once");
|
||||||
|
|
||||||
Use(new DependencyResolverBinding());
|
Use(new DependencyResolverBinding());
|
||||||
Use(new MessageBinding());
|
|
||||||
Use(new PublishResultBinding());
|
Use(new PublishResultBinding());
|
||||||
}
|
|
||||||
|
|
||||||
|
// Registered last so it runs first and the MessageClass is known to other middleware
|
||||||
public IConfig Build()
|
Use(new MessageBinding());
|
||||||
{
|
|
||||||
RegisterCustomBindings();
|
|
||||||
|
|
||||||
RegisterDefaults();
|
RegisterDefaults();
|
||||||
|
(config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<ITapetiConfig>(config);
|
||||||
var queues = new List<IQueue>();
|
|
||||||
queues.AddRange(staticRegistrations.Select(qb => new Queue(new QueueInfo { Dynamic = false, Name = qb.Key }, qb.Value)));
|
|
||||||
|
|
||||||
|
|
||||||
// We want to ensure each queue only has unique messages classes. This means we can requeue
|
var outputConfig = config;
|
||||||
// without the side-effect of calling other handlers for the same message class again as well.
|
config = null;
|
||||||
//
|
|
||||||
// Since I had trouble deciphering this code after a year, here's an overview of how it achieves this grouping
|
|
||||||
// and how the bindingIndex is relevant:
|
|
||||||
//
|
|
||||||
// dynamicRegistrations:
|
|
||||||
// Key (prefix)
|
|
||||||
// ""
|
|
||||||
// Key (message class) Value (list of bindings)
|
|
||||||
// A binding1, binding2, binding3
|
|
||||||
// B binding4
|
|
||||||
// "prefix"
|
|
||||||
// A binding5, binding6
|
|
||||||
//
|
|
||||||
// By combining all bindings with the same index, per prefix, the following queues will be registered:
|
|
||||||
//
|
|
||||||
// Prefix Bindings
|
|
||||||
// "" binding1 (message A), binding4 (message B)
|
|
||||||
// "" binding2 (message A)
|
|
||||||
// "" binding3 (message A)
|
|
||||||
// "prefix" binding5 (message A)
|
|
||||||
// "prefix" binding6 (message A)
|
|
||||||
//
|
|
||||||
foreach (var prefixGroup in dynamicRegistrations)
|
|
||||||
{
|
|
||||||
var dynamicBindings = new List<List<IBinding>>();
|
|
||||||
|
|
||||||
foreach (var bindings in prefixGroup.Value.Values)
|
outputConfig.Lock();
|
||||||
{
|
return outputConfig;
|
||||||
while (dynamicBindings.Count < bindings.Count)
|
|
||||||
dynamicBindings.Add(new List<IBinding>());
|
|
||||||
|
|
||||||
for (var bindingIndex = 0; bindingIndex < bindings.Count; bindingIndex++)
|
|
||||||
dynamicBindings[bindingIndex].Add(bindings[bindingIndex]);
|
|
||||||
}
|
|
||||||
|
|
||||||
queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true, Name = GetDynamicQueueName(prefixGroup.Key) }, bl)));
|
|
||||||
}
|
|
||||||
|
|
||||||
queues.AddRange(uniqueRegistrations.Select(b => new Queue(new QueueInfo { Dynamic = true, Name = GetDynamicQueueName(b.QueueInfo.Name) }, new []{b})));
|
|
||||||
|
|
||||||
|
|
||||||
var config = new Config(queues)
|
|
||||||
{
|
|
||||||
DependencyResolver = dependencyResolver,
|
|
||||||
MessageMiddleware = messageMiddleware,
|
|
||||||
CleanupMiddleware = cleanupMiddleware,
|
|
||||||
PublishMiddleware = publishMiddleware,
|
|
||||||
|
|
||||||
UsePublisherConfirms = usePublisherConfirms
|
|
||||||
};
|
|
||||||
|
|
||||||
(dependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<IConfig>(config);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig Use(IBindingMiddleware handler)
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler)
|
||||||
{
|
{
|
||||||
bindingMiddleware.Add(handler);
|
bindingMiddleware.Add(handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig Use(IMessageMiddleware handler)
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder Use(IMessageMiddleware handler)
|
||||||
{
|
{
|
||||||
messageMiddleware.Add(handler);
|
GetConfig().Use(handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig Use(ICleanupMiddleware handler)
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder Use(IPublishMiddleware handler)
|
||||||
{
|
{
|
||||||
cleanupMiddleware.Add(handler);
|
GetConfig().Use(handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig Use(IPublishMiddleware handler)
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder Use(ITapetiExtension extension)
|
||||||
{
|
{
|
||||||
publishMiddleware.Add(handler);
|
if (DependencyResolver is IDependencyContainer container)
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig Use(ITapetiExtension extension)
|
|
||||||
{
|
|
||||||
if (dependencyResolver is IDependencyContainer container)
|
|
||||||
extension.RegisterDefaults(container);
|
extension.RegisterDefaults(container);
|
||||||
|
|
||||||
var middlewareBundle = extension.GetMiddleware(dependencyResolver);
|
var configInstance = GetConfig();
|
||||||
|
|
||||||
if (extension is ITapetiExtentionBinding extentionBindings)
|
var middlewareBundle = extension.GetMiddleware(DependencyResolver);
|
||||||
customBindings.AddRange(extentionBindings.GetBindings(dependencyResolver));
|
|
||||||
|
|
||||||
// ReSharper disable once InvertIf
|
|
||||||
if (middlewareBundle != null)
|
if (middlewareBundle != null)
|
||||||
{
|
{
|
||||||
foreach (var middleware in middlewareBundle)
|
foreach (var middleware in middlewareBundle)
|
||||||
{
|
{
|
||||||
// ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
|
switch (middleware)
|
||||||
if (middleware is IBindingMiddleware bindingExtension)
|
{
|
||||||
|
case IControllerBindingMiddleware bindingExtension:
|
||||||
Use(bindingExtension);
|
Use(bindingExtension);
|
||||||
else if (middleware is IMessageMiddleware messageExtension)
|
break;
|
||||||
Use(messageExtension);
|
|
||||||
else if (middleware is ICleanupMiddleware cleanupExtension)
|
case IMessageMiddleware messageExtension:
|
||||||
Use(cleanupExtension);
|
configInstance.Use(messageExtension);
|
||||||
else if (middleware is IPublishMiddleware publishExtension)
|
break;
|
||||||
Use(publishExtension);
|
|
||||||
else
|
case IPublishMiddleware publishExtension:
|
||||||
throw new ArgumentException($"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}");
|
configInstance.Use(publishExtension);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver);
|
||||||
|
if (bindingBundle == null)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
foreach (var binding in bindingBundle)
|
||||||
|
config.RegisterBinding(binding);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RegisterBinding(IBinding binding)
|
||||||
|
{
|
||||||
|
GetConfig().RegisterBinding(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder DisablePublisherConfirms()
|
||||||
|
{
|
||||||
|
GetConfig().SetPublisherConfirms(false);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder SetPublisherConfirms(bool enabled)
|
||||||
|
{
|
||||||
|
GetConfig().SetPublisherConfirms(enabled);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder EnableDeclareDurableQueues()
|
||||||
|
{
|
||||||
|
GetConfig().SetDeclareDurableQueues(true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfigBuilder SetDeclareDurableQueues(bool enabled)
|
||||||
|
{
|
||||||
|
GetConfig().SetDeclareDurableQueues(enabled);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WARNING: disabling publisher confirms means there is no guarantee that a Publish succeeds,
|
/// Registers the default implementation of various Tapeti interfaces into the IoC container.
|
||||||
/// and disables Tapeti.Flow from verifying if a request/response can be routed. This may
|
|
||||||
/// result in never-ending flows. Only disable if you can accept those consequences.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TapetiConfig DisablePublisherConfirms()
|
protected void RegisterDefaults()
|
||||||
{
|
{
|
||||||
usePublisherConfirms = false;
|
if (!(DependencyResolver is IDependencyContainer container))
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WARNING: disabling publisher confirms means there is no guarantee that a Publish succeeds,
|
|
||||||
/// and disables Tapeti.Flow from verifying if a request/response can be routed. This may
|
|
||||||
/// result in never-ending flows. Only disable if you accept those consequences.
|
|
||||||
/// </summary>
|
|
||||||
public TapetiConfig SetPublisherConfirms(bool enabled)
|
|
||||||
{
|
|
||||||
usePublisherConfirms = enabled;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void RegisterDefaults()
|
|
||||||
{
|
|
||||||
if (!(dependencyResolver is IDependencyContainer container))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (ConsoleHelper.IsAvailable())
|
if (ConsoleHelper.IsAvailable())
|
||||||
@ -218,85 +187,133 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig RegisterController(Type controller)
|
/// <inheritdoc />
|
||||||
|
public void ApplyBindingMiddleware(IControllerBindingContext context, Action lastHandler)
|
||||||
{
|
{
|
||||||
var controllerQueueInfo = GetQueueInfo(controller);
|
MiddlewareHelper.Go(bindingMiddleware,
|
||||||
|
(handler, next) => handler.Handle(context, next),
|
||||||
if (!controller.IsInterface)
|
lastHandler);
|
||||||
(dependencyResolver as IDependencyContainer)?.RegisterController(controller);
|
|
||||||
|
|
||||||
foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
|
||||||
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false)
|
|
||||||
.Select(m => (MethodInfo)m))
|
|
||||||
{
|
|
||||||
var context = new BindingContext(method);
|
|
||||||
var messageHandler = GetMessageHandler(context, method);
|
|
||||||
if (messageHandler == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
|
|
||||||
if (!methodQueueInfo.IsValid)
|
|
||||||
throw new TopologyConfigurationException(
|
|
||||||
$"Method {method.Name} or controller {controller.Name} requires a queue attribute");
|
|
||||||
|
|
||||||
var handlerInfo = new Binding
|
|
||||||
{
|
|
||||||
Controller = controller,
|
|
||||||
Method = method,
|
|
||||||
QueueInfo = methodQueueInfo,
|
|
||||||
QueueBindingMode = context.QueueBindingMode,
|
|
||||||
MessageClass = context.MessageClass,
|
|
||||||
MessageHandler = messageHandler,
|
|
||||||
MessageMiddleware = context.MessageMiddleware,
|
|
||||||
MessageFilterMiddleware = context.MessageFilterMiddleware
|
|
||||||
};
|
|
||||||
|
|
||||||
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
|
||||||
AddDynamicRegistration(handlerInfo);
|
|
||||||
else
|
|
||||||
AddStaticRegistration(handlerInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig RegisterAllControllers(Assembly assembly)
|
private Config GetConfig()
|
||||||
{
|
{
|
||||||
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute))))
|
if (config == null)
|
||||||
RegisterController(type);
|
throw new InvalidOperationException("TapetiConfig can not be updated after Build");
|
||||||
|
|
||||||
return this;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig RegisterAllControllers()
|
/// <inheritdoc />
|
||||||
|
internal class Config : ITapetiConfig
|
||||||
{
|
{
|
||||||
return RegisterAllControllers(Assembly.GetEntryAssembly());
|
private readonly ConfigFeatures features = new ConfigFeatures();
|
||||||
|
private readonly ConfigMiddleware middleware = new ConfigMiddleware();
|
||||||
|
private readonly ConfigBindings bindings = new ConfigBindings();
|
||||||
|
|
||||||
|
public IDependencyResolver DependencyResolver { get; }
|
||||||
|
public ITapetiConfigFeatues Features => features;
|
||||||
|
public ITapetiConfigMiddleware Middleware => middleware;
|
||||||
|
public ITapetiConfigBindings Bindings => bindings;
|
||||||
|
|
||||||
|
|
||||||
|
public Config(IDependencyResolver dependencyResolver)
|
||||||
|
{
|
||||||
|
DependencyResolver = dependencyResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterCustomBindings()
|
|
||||||
{
|
|
||||||
foreach (var customBinding in customBindings)
|
|
||||||
{
|
|
||||||
// TODO Do we need to configure additional middleware, or does this only get confused if there is no MessageClass
|
|
||||||
|
|
||||||
var binding = new CustomBinding(customBinding);
|
public void Lock()
|
||||||
if (binding.QueueInfo.Dynamic == false)
|
|
||||||
{
|
{
|
||||||
AddStaticRegistration(binding);
|
bindings.Lock();
|
||||||
}
|
}
|
||||||
else if (binding.MessageClass != null)
|
|
||||||
|
|
||||||
|
public void Use(IMessageMiddleware handler)
|
||||||
{
|
{
|
||||||
AddDynamicRegistration(binding);
|
middleware.Use(handler);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
public void Use(IPublishMiddleware handler)
|
||||||
{
|
{
|
||||||
AddUniqueRegistration(binding);
|
middleware.Use(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void RegisterBinding(IBinding binding)
|
||||||
|
{
|
||||||
|
bindings.Add(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetPublisherConfirms(bool enabled)
|
||||||
|
{
|
||||||
|
features.PublisherConfirms = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDeclareDurableQueues(bool enabled)
|
||||||
|
{
|
||||||
|
features.DeclareDurableQueues = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class ConfigFeatures : ITapetiConfigFeatues
|
||||||
|
{
|
||||||
|
public bool PublisherConfirms { get; internal set; } = true;
|
||||||
|
public bool DeclareDurableQueues { get; internal set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class ConfigMiddleware : ITapetiConfigMiddleware
|
||||||
|
{
|
||||||
|
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
|
||||||
|
private readonly List<IPublishMiddleware> publishMiddleware = new List<IPublishMiddleware>();
|
||||||
|
|
||||||
|
|
||||||
|
public IReadOnlyList<IMessageMiddleware> Message => messageMiddleware;
|
||||||
|
public IReadOnlyList<IPublishMiddleware> Publish => publishMiddleware;
|
||||||
|
|
||||||
|
|
||||||
|
public void Use(IMessageMiddleware handler)
|
||||||
|
{
|
||||||
|
messageMiddleware.Add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Use(IPublishMiddleware handler)
|
||||||
|
{
|
||||||
|
publishMiddleware.Add(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class ConfigBindings : List<IBinding>, ITapetiConfigBindings
|
||||||
|
{
|
||||||
|
private Dictionary<MethodInfo, IControllerMethodBinding> methodLookup;
|
||||||
|
|
||||||
|
|
||||||
|
public IControllerMethodBinding ForMethod(Delegate method)
|
||||||
|
{
|
||||||
|
return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Lock()
|
||||||
|
{
|
||||||
|
methodLookup = this
|
||||||
|
.Where(binding => binding is IControllerMethodBinding)
|
||||||
|
.Cast<IControllerMethodBinding>()
|
||||||
|
.ToDictionary(binding => binding.Method, binding => binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
public delegate Task MessageHandlerFunc(IMessageContext context, object message);
|
||||||
|
|
||||||
|
|
||||||
protected MessageHandlerFunc GetMessageHandler(IBindingContext context, MethodInfo method)
|
protected MessageHandlerFunc GetMessageHandler(IBindingContext context, MethodInfo method)
|
||||||
{
|
{
|
||||||
var allowBinding= false;
|
var allowBinding= false;
|
||||||
@ -408,48 +425,6 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void AddDynamicRegistration(IBindingQueueInfo binding)
|
|
||||||
{
|
|
||||||
var prefix = binding.QueueInfo.Name ?? "";
|
|
||||||
|
|
||||||
if (!dynamicRegistrations.TryGetValue(prefix, out Dictionary<Type, List<IBinding>> prefixRegistrations))
|
|
||||||
{
|
|
||||||
prefixRegistrations = new Dictionary<Type, List<IBinding>>();
|
|
||||||
dynamicRegistrations.Add(prefix, prefixRegistrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefixRegistrations.TryGetValue(binding.MessageClass, out List<IBinding> bindings))
|
|
||||||
{
|
|
||||||
bindings = new List<IBinding>();
|
|
||||||
prefixRegistrations.Add(binding.MessageClass, bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindings.Add(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AddUniqueRegistration(IBindingQueueInfo binding)
|
|
||||||
{
|
|
||||||
uniqueRegistrations.Add(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected QueueInfo GetQueueInfo(MemberInfo member)
|
|
||||||
{
|
|
||||||
var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
|
|
||||||
var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>();
|
|
||||||
|
|
||||||
if (dynamicQueueAttribute != null && durableQueueAttribute != null)
|
|
||||||
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on {member.Name}");
|
|
||||||
|
|
||||||
if (dynamicQueueAttribute != null)
|
|
||||||
return new QueueInfo { Dynamic = true, Name = dynamicQueueAttribute.Prefix };
|
|
||||||
|
|
||||||
if (durableQueueAttribute != null)
|
|
||||||
return new QueueInfo { Dynamic = false, Name = durableQueueAttribute.Name };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected string GetDynamicQueueName(string prefix)
|
protected string GetDynamicQueueName(string prefix)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrEmpty(prefix))
|
if (String.IsNullOrEmpty(prefix))
|
||||||
@ -457,300 +432,6 @@ namespace Tapeti
|
|||||||
|
|
||||||
return prefix + "." + Guid.NewGuid().ToString("N");
|
return prefix + "." + Guid.NewGuid().ToString("N");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected class QueueInfo
|
|
||||||
{
|
|
||||||
public bool? Dynamic { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public bool IsValid => Dynamic.HasValue || !string.IsNullOrEmpty(Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected class Config : IConfig
|
|
||||||
{
|
|
||||||
public bool UsePublisherConfirms { get; set; }
|
|
||||||
|
|
||||||
public IDependencyResolver DependencyResolver { get; set; }
|
|
||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
|
|
||||||
public IReadOnlyList<ICleanupMiddleware> CleanupMiddleware { get; set; }
|
|
||||||
public IReadOnlyList<IPublishMiddleware> PublishMiddleware { get; set; }
|
|
||||||
public IEnumerable<IQueue> Queues { get; }
|
|
||||||
|
|
||||||
private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup;
|
|
||||||
|
|
||||||
|
|
||||||
public Config(IEnumerable<IQueue> queues)
|
|
||||||
{
|
|
||||||
Queues = queues.ToList();
|
|
||||||
|
|
||||||
bindingMethodLookup = Queues.SelectMany(q => q.Bindings).ToDictionary(b => b.Method, b => b);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public IBinding GetBinding(Delegate method)
|
|
||||||
{
|
|
||||||
return bindingMethodLookup.TryGetValue(method.Method, out var binding) ? binding : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected class Queue : IDynamicQueue
|
|
||||||
{
|
|
||||||
private readonly string declareQueueName;
|
|
||||||
|
|
||||||
public bool Dynamic { get; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public IEnumerable<IBinding> Bindings { get; }
|
|
||||||
|
|
||||||
|
|
||||||
public Queue(QueueInfo queue, IEnumerable<IBinding> bindings)
|
|
||||||
{
|
|
||||||
declareQueueName = queue.Name;
|
|
||||||
|
|
||||||
Dynamic = queue.Dynamic.GetValueOrDefault();
|
|
||||||
Name = queue.Name;
|
|
||||||
Bindings = bindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public string GetDeclareQueueName()
|
|
||||||
{
|
|
||||||
return declareQueueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void SetName(string name)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected interface IBindingQueueInfo : IBuildBinding
|
|
||||||
{
|
|
||||||
QueueInfo QueueInfo { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class Binding : IBindingQueueInfo
|
|
||||||
{
|
|
||||||
public Type Controller { get; set; }
|
|
||||||
public MethodInfo Method { get; set; }
|
|
||||||
public Type MessageClass { get; set; }
|
|
||||||
public string QueueName { get; set; }
|
|
||||||
public QueueBindingMode QueueBindingMode { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
|
|
||||||
public IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; set; }
|
|
||||||
|
|
||||||
private QueueInfo queueInfo;
|
|
||||||
public QueueInfo QueueInfo
|
|
||||||
{
|
|
||||||
get => queueInfo;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
QueueName = (value?.Dynamic).GetValueOrDefault() ? value?.Name : null;
|
|
||||||
queueInfo = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageHandlerFunc MessageHandler { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public void SetQueueName(string queueName)
|
|
||||||
{
|
|
||||||
QueueName = queueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public bool Accept(Type messageClass)
|
|
||||||
{
|
|
||||||
return MessageClass.IsAssignableFrom(messageClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Accept(IMessageContext context, object message)
|
|
||||||
{
|
|
||||||
return message.GetType() == MessageClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task Invoke(IMessageContext context, object message)
|
|
||||||
{
|
|
||||||
return MessageHandler(context, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected class CustomBinding : IBindingQueueInfo
|
|
||||||
{
|
|
||||||
private readonly ICustomBinding inner;
|
|
||||||
|
|
||||||
public CustomBinding(ICustomBinding inner)
|
|
||||||
{
|
|
||||||
this.inner = inner;
|
|
||||||
|
|
||||||
// Copy all variables to make them guaranteed readonly.
|
|
||||||
Controller = inner.Controller;
|
|
||||||
Method = inner.Method;
|
|
||||||
QueueBindingMode = inner.QueueBindingMode;
|
|
||||||
MessageClass = inner.MessageClass;
|
|
||||||
|
|
||||||
QueueInfo = inner.StaticQueueName != null
|
|
||||||
? new QueueInfo()
|
|
||||||
{
|
|
||||||
Dynamic = false,
|
|
||||||
Name = inner.StaticQueueName
|
|
||||||
}
|
|
||||||
: new QueueInfo()
|
|
||||||
{
|
|
||||||
Dynamic = true,
|
|
||||||
Name = inner.DynamicQueuePrefix
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom bindings cannot have other middleware messing with the binding.
|
|
||||||
MessageFilterMiddleware = new IMessageFilterMiddleware[0];
|
|
||||||
MessageMiddleware = new IMessageMiddleware[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type Controller { get; }
|
|
||||||
public MethodInfo Method { get; }
|
|
||||||
public string QueueName { get; private set; }
|
|
||||||
public QueueBindingMode QueueBindingMode { get; set; }
|
|
||||||
public IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
|
|
||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
|
||||||
|
|
||||||
public bool Accept(Type messageClass)
|
|
||||||
{
|
|
||||||
return inner.Accept(messageClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Accept(IMessageContext context, object message)
|
|
||||||
{
|
|
||||||
return inner.Accept(context, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Invoke(IMessageContext context, object message)
|
|
||||||
{
|
|
||||||
return inner.Invoke(context, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetQueueName(string queueName)
|
|
||||||
{
|
|
||||||
QueueName = queueName;
|
|
||||||
inner.SetQueueName(queueName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type MessageClass { get; }
|
|
||||||
public QueueInfo QueueInfo { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface IBindingParameterAccess
|
|
||||||
{
|
|
||||||
ValueFactory GetBinding();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal interface IBindingResultAccess
|
|
||||||
{
|
|
||||||
ResultHandler GetHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class BindingContext : IBindingContext
|
|
||||||
{
|
|
||||||
private List<IMessageMiddleware> messageMiddleware;
|
|
||||||
private List<IMessageFilterMiddleware> messageFilterMiddleware;
|
|
||||||
|
|
||||||
public Type MessageClass { get; set; }
|
|
||||||
|
|
||||||
public MethodInfo Method { get; }
|
|
||||||
public IReadOnlyList<IBindingParameter> Parameters { get; }
|
|
||||||
public IBindingResult Result { get; }
|
|
||||||
|
|
||||||
public QueueBindingMode QueueBindingMode { get; set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware => messageMiddleware;
|
|
||||||
public IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware => messageFilterMiddleware;
|
|
||||||
|
|
||||||
|
|
||||||
public BindingContext(MethodInfo method)
|
|
||||||
{
|
|
||||||
Method = method;
|
|
||||||
|
|
||||||
Parameters = method.GetParameters().Select(p => new BindingParameter(p)).ToList();
|
|
||||||
Result = new BindingResult(method.ReturnParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Use(IMessageMiddleware middleware)
|
|
||||||
{
|
|
||||||
if (messageMiddleware == null)
|
|
||||||
messageMiddleware = new List<IMessageMiddleware>();
|
|
||||||
|
|
||||||
messageMiddleware.Add(middleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Use(IMessageFilterMiddleware filterMiddleware)
|
|
||||||
{
|
|
||||||
if (messageFilterMiddleware == null)
|
|
||||||
messageFilterMiddleware = new List<IMessageFilterMiddleware>();
|
|
||||||
|
|
||||||
messageFilterMiddleware.Add(filterMiddleware);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class BindingParameter : IBindingParameter, IBindingParameterAccess
|
|
||||||
{
|
|
||||||
private ValueFactory binding;
|
|
||||||
|
|
||||||
public ParameterInfo Info { get; }
|
|
||||||
public bool HasBinding => binding != null;
|
|
||||||
|
|
||||||
|
|
||||||
public BindingParameter(ParameterInfo parameter)
|
|
||||||
{
|
|
||||||
Info = parameter;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueFactory GetBinding()
|
|
||||||
{
|
|
||||||
return binding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetBinding(ValueFactory valueFactory)
|
|
||||||
{
|
|
||||||
binding = valueFactory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class BindingResult : IBindingResult, IBindingResultAccess
|
|
||||||
{
|
|
||||||
private ResultHandler handler;
|
|
||||||
|
|
||||||
public ParameterInfo Info { get; }
|
|
||||||
public bool HasHandler => handler != null;
|
|
||||||
|
|
||||||
|
|
||||||
public BindingResult(ParameterInfo parameter)
|
|
||||||
{
|
|
||||||
Info = parameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ResultHandler GetHandler()
|
|
||||||
{
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetHandler(ResultHandler resultHandler)
|
|
||||||
{
|
|
||||||
handler = resultHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
127
Tapeti/TapetiConfigControllers.cs
Normal file
127
Tapeti/TapetiConfigControllers.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Tapeti.Annotations;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Default;
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when an issue is detected in a controller configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class TopologyConfigurationException : Exception
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TopologyConfigurationException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering message controllers.
|
||||||
|
/// </summary>
|
||||||
|
public static class TapetiConfigControllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all public methods in the specified controller class as message handlers.
|
||||||
|
/// </summary>
|
||||||
|
/// <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>
|
||||||
|
public static ITapetiConfigBuilder RegisterController(this ITapetiConfigBuilder builder, Type controller)
|
||||||
|
{
|
||||||
|
var builderAccess = (ITapetiConfigBuilderAccess)builder;
|
||||||
|
|
||||||
|
if (!controller.IsClass)
|
||||||
|
throw new ArgumentException($"Controller {controller.Name} must be a class");
|
||||||
|
|
||||||
|
var controllerQueueInfo = GetQueueInfo(controller);
|
||||||
|
(builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller);
|
||||||
|
|
||||||
|
foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object) && (m as MethodInfo)?.IsSpecialName == false)
|
||||||
|
.Select(m => (MethodInfo)m))
|
||||||
|
{
|
||||||
|
// TODO create binding for method
|
||||||
|
|
||||||
|
/*
|
||||||
|
var context = new BindingContext(method);
|
||||||
|
var messageHandler = GetMessageHandler(context, method);
|
||||||
|
if (messageHandler == null)
|
||||||
|
continue;
|
||||||
|
*/
|
||||||
|
|
||||||
|
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
|
||||||
|
if (methodQueueInfo == null || !methodQueueInfo.IsValid)
|
||||||
|
throw new TopologyConfigurationException(
|
||||||
|
$"Method {method.Name} or controller {controller.Name} requires a queue attribute");
|
||||||
|
|
||||||
|
/*
|
||||||
|
var handlerInfo = new Binding
|
||||||
|
{
|
||||||
|
Controller = controller,
|
||||||
|
Method = method,
|
||||||
|
QueueInfo = methodQueueInfo,
|
||||||
|
QueueBindingMode = context.QueueBindingMode,
|
||||||
|
MessageClass = context.MessageClass,
|
||||||
|
MessageHandler = messageHandler,
|
||||||
|
MessageMiddleware = context.MessageMiddleware,
|
||||||
|
MessageFilterMiddleware = context.MessageFilterMiddleware
|
||||||
|
};
|
||||||
|
|
||||||
|
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
||||||
|
AddDynamicRegistration(handlerInfo);
|
||||||
|
else
|
||||||
|
AddStaticRegistration(handlerInfo);
|
||||||
|
*/
|
||||||
|
|
||||||
|
builder.RegisterBinding(new ControllerMethodBinding(controller, method, methodQueueInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all controllers in the specified assembly which are marked with the MessageController attribute.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder"></param>
|
||||||
|
/// <param name="assembly">The assembly to scan for controllers.</param>
|
||||||
|
public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder, Assembly assembly)
|
||||||
|
{
|
||||||
|
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute))))
|
||||||
|
RegisterController(builder, type);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all controllers in the entry assembly which are marked with the MessageController attribute.
|
||||||
|
/// </summary>
|
||||||
|
/// <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>();
|
||||||
|
|
||||||
|
if (dynamicQueueAttribute != null && durableQueueAttribute != null)
|
||||||
|
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
|
||||||
|
|
||||||
|
if (dynamicQueueAttribute != null)
|
||||||
|
return new ControllerMethodBinding.QueueInfo { Dynamic = true, Name = dynamicQueueAttribute.Prefix };
|
||||||
|
|
||||||
|
return durableQueueAttribute != null
|
||||||
|
? new ControllerMethodBinding.QueueInfo { Dynamic = false, Name = durableQueueAttribute.Name }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +1,68 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Tapeti.Config;
|
using Tapeti.Config;
|
||||||
using Tapeti.Connection;
|
using Tapeti.Connection;
|
||||||
|
|
||||||
// ReSharper disable UnusedMember.Global
|
// ReSharper disable UnusedMember.Global
|
||||||
|
|
||||||
|
// TODO more separation from the actual worker / RabbitMQ Client for unit testing purposes
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
public delegate void DisconnectedEventHandler(object sender, DisconnectedEventArgs e);
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
public class TapetiConnection : IDisposable
|
/// Creates a connection to RabbitMQ based on the provided Tapeti config.
|
||||||
|
/// </summary>
|
||||||
|
public class TapetiConnection : IConnection
|
||||||
{
|
{
|
||||||
private readonly IConfig config;
|
private readonly ITapetiConfig config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the hostname and credentials to use when connecting to RabbitMQ.
|
||||||
|
/// Defaults to guest on localhost.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property must be set before first subscribing or publishing, otherwise it
|
||||||
|
/// will use the default connection parameters.
|
||||||
|
/// </remarks>
|
||||||
public TapetiConnectionParams Params { get; set; }
|
public TapetiConnectionParams Params { get; set; }
|
||||||
|
|
||||||
private readonly Lazy<TapetiWorker> worker;
|
private readonly Lazy<TapetiClient> client;
|
||||||
private TapetiSubscriber subscriber;
|
private TapetiSubscriber subscriber;
|
||||||
|
|
||||||
public TapetiConnection(IConfig config)
|
/// <summary>
|
||||||
|
/// Creates a new instance of a TapetiConnection and registers a default IPublisher
|
||||||
|
/// in the IoC container as provided in the config.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config"></param>
|
||||||
|
public TapetiConnection(ITapetiConfig config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
(config.DependencyResolver as IDependencyContainer)?.RegisterDefault(GetPublisher);
|
(config.DependencyResolver as IDependencyContainer)?.RegisterDefault(GetPublisher);
|
||||||
|
|
||||||
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config)
|
client = new Lazy<TapetiClient>(() => new TapetiClient(config, Params ?? new TapetiConnectionParams())
|
||||||
{
|
{
|
||||||
ConnectionParams = Params ?? new TapetiConnectionParams(),
|
|
||||||
ConnectionEventListener = new ConnectionEventListener(this)
|
ConnectionEventListener = new ConnectionEventListener(this)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public event EventHandler Connected;
|
public event EventHandler Connected;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public event DisconnectedEventHandler Disconnected;
|
public event DisconnectedEventHandler Disconnected;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public event EventHandler Reconnected;
|
public event EventHandler Reconnected;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<ISubscriber> Subscribe(bool startConsuming = true)
|
public async Task<ISubscriber> Subscribe(bool startConsuming = true)
|
||||||
{
|
{
|
||||||
if (subscriber == null)
|
if (subscriber == null)
|
||||||
{
|
{
|
||||||
subscriber = new TapetiSubscriber(() => worker.Value, config.Queues.ToList());
|
subscriber = new TapetiSubscriber(() => client.Value, config);
|
||||||
await subscriber.BindQueues();
|
await subscriber.ApplyBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startConsuming)
|
if (startConsuming)
|
||||||
@ -50,30 +72,35 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ISubscriber SubscribeSync(bool startConsuming = true)
|
public ISubscriber SubscribeSync(bool startConsuming = true)
|
||||||
{
|
{
|
||||||
return Subscribe(startConsuming).Result;
|
return Subscribe(startConsuming).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IPublisher GetPublisher()
|
public IPublisher GetPublisher()
|
||||||
{
|
{
|
||||||
return new TapetiPublisher(() => worker.Value);
|
return new TapetiPublisher(config, () => client.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task Close()
|
public async Task Close()
|
||||||
{
|
{
|
||||||
if (worker.IsValueCreated)
|
if (client.IsValueCreated)
|
||||||
await worker.Value.Close();
|
await client.Value.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Close().Wait();
|
Close().Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class ConnectionEventListener: IConnectionEventListener
|
private class ConnectionEventListener: IConnectionEventListener
|
||||||
{
|
{
|
||||||
private readonly TapetiConnection owner;
|
private readonly TapetiConnection owner;
|
||||||
@ -99,25 +126,47 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a connection to RabbitMQ has been established.
|
||||||
|
/// </summary>
|
||||||
protected virtual void OnConnected(EventArgs e)
|
protected virtual void OnConnected(EventArgs e)
|
||||||
{
|
{
|
||||||
Task.Run(() => Connected?.Invoke(this, e));
|
var connectedEvent = Connected;
|
||||||
|
if (connectedEvent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Task.Run(() => connectedEvent.Invoke(this, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the connection to RabbitMQ has been lost.
|
||||||
|
/// </summary>
|
||||||
protected virtual void OnReconnected(EventArgs e)
|
protected virtual void OnReconnected(EventArgs e)
|
||||||
{
|
{
|
||||||
|
var reconnectedEvent = Reconnected;
|
||||||
|
if (reconnectedEvent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
subscriber?.RebindQueues().ContinueWith((t) =>
|
subscriber?.ApplyBindings().ContinueWith((t) =>
|
||||||
{
|
{
|
||||||
Reconnected?.Invoke(this, e);
|
reconnectedEvent.Invoke(this, e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the connection to RabbitMQ has been recovered after an unexpected disconnect.
|
||||||
|
/// </summary>
|
||||||
protected virtual void OnDisconnected(DisconnectedEventArgs e)
|
protected virtual void OnDisconnected(DisconnectedEventArgs e)
|
||||||
{
|
{
|
||||||
Task.Run(() => Disconnected?.Invoke(this, e));
|
var disconnectedEvent = Disconnected;
|
||||||
|
if (disconnectedEvent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Task.Run(() => disconnectedEvent.Invoke(this, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,34 @@
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
public class TapetiConnectionParams
|
public class TapetiConnectionParams
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The hostname to connect to. Defaults to localhost.
|
||||||
|
/// </summary>
|
||||||
public string HostName { get; set; } = "localhost";
|
public string HostName { get; set; } = "localhost";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The port to connect to. Defaults to 5672.
|
||||||
|
/// </summary>
|
||||||
public int Port { get; set; } = 5672;
|
public int Port { get; set; } = 5672;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The virtual host in RabbitMQ. Defaults to /.
|
||||||
|
/// </summary>
|
||||||
public string VirtualHost { get; set; } = "/";
|
public string VirtualHost { get; set; } = "/";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The username to authenticate with. Defaults to guest.
|
||||||
|
/// </summary>
|
||||||
public string Username { get; set; } = "guest";
|
public string Username { get; set; } = "guest";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The password to authenticate with. Defaults to guest.
|
||||||
|
/// </summary>
|
||||||
public string Password { get; set; } = "guest";
|
public string Password { get; set; } = "guest";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -20,10 +42,17 @@ namespace Tapeti
|
|||||||
public ushort PrefetchCount { get; set; } = 50;
|
public ushort PrefetchCount { get; set; } = 50;
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public TapetiConnectionParams()
|
public TapetiConnectionParams()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a new TapetiConnectionParams instance based on standard URI syntax.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>new TapetiConnectionParams(new Uri("amqp://username:password@hostname/"))</example>
|
||||||
|
/// <example>new TapetiConnectionParams(new Uri("amqp://username:password@hostname:5672/virtualHost"))</example>
|
||||||
|
/// <param name="uri"></param>
|
||||||
public TapetiConnectionParams(Uri uri)
|
public TapetiConnectionParams(Uri uri)
|
||||||
{
|
{
|
||||||
HostName = uri.Host;
|
HostName = uri.Host;
|
||||||
|
@ -6,6 +6,10 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Tapeti.Tasks
|
namespace Tapeti.Tasks
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// An implementation of a queue which runs tasks on a single thread.
|
||||||
|
/// </summary>
|
||||||
public class SingleThreadTaskQueue : IDisposable
|
public class SingleThreadTaskQueue : IDisposable
|
||||||
{
|
{
|
||||||
private readonly object previousTaskLock = new object();
|
private readonly object previousTaskLock = new object();
|
||||||
@ -14,6 +18,10 @@ namespace Tapeti.Tasks
|
|||||||
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new Lazy<SingleThreadTaskScheduler>();
|
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new Lazy<SingleThreadTaskScheduler>();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the specified synchronous action to the task queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action"></param>
|
||||||
public Task Add(Action action)
|
public Task Add(Action action)
|
||||||
{
|
{
|
||||||
lock (previousTaskLock)
|
lock (previousTaskLock)
|
||||||
@ -27,6 +35,10 @@ namespace Tapeti.Tasks
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the specified asynchronous method to the task queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="func"></param>
|
||||||
public Task Add(Func<Task> func)
|
public Task Add(Func<Task> func)
|
||||||
{
|
{
|
||||||
lock (previousTaskLock)
|
lock (previousTaskLock)
|
||||||
@ -45,15 +57,15 @@ namespace Tapeti.Tasks
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (singleThreadScheduler.IsValueCreated)
|
if (singleThreadScheduler.IsValueCreated)
|
||||||
singleThreadScheduler.Value.Dispose();
|
singleThreadScheduler.Value.Dispose();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class SingleThreadTaskScheduler : TaskScheduler, IDisposable
|
internal class SingleThreadTaskScheduler : TaskScheduler, IDisposable
|
||||||
{
|
{
|
||||||
public override int MaximumConcurrencyLevel => 1;
|
public override int MaximumConcurrencyLevel => 1;
|
||||||
|
|
||||||
@ -107,7 +119,7 @@ namespace Tapeti.Tasks
|
|||||||
|
|
||||||
private void WorkerThread()
|
private void WorkerThread()
|
||||||
{
|
{
|
||||||
while(true)
|
while (true)
|
||||||
{
|
{
|
||||||
Task task;
|
Task task;
|
||||||
lock (scheduledTasks)
|
lock (scheduledTasks)
|
||||||
@ -130,4 +142,5 @@ namespace Tapeti.Tasks
|
|||||||
return disposed ? null : scheduledTasks.Dequeue();
|
return disposed ? null : scheduledTasks.Dequeue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user