[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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -4,6 +4,10 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -8,15 +8,15 @@ using Tapeti.Helpers;
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
if (context.Method.GetCustomAttribute<ContinuationAttribute>() != null)
|
||||
context.QueueBindingMode = QueueBindingMode.DirectToQueue;
|
||||
context.SetBindingTargetMode(BindingTargetMode.Direct);
|
||||
|
||||
RegisterYieldPointResult(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>();
|
||||
if (continuationAttribute == null)
|
||||
return;
|
||||
|
||||
context.Use(new FlowMessageFilterMiddleware());
|
||||
context.Use(new FlowMessageMiddleware());
|
||||
context.Use(new FlowMiddleware());
|
||||
|
||||
if (context.Result.HasHandler)
|
||||
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))
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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 =>
|
||||
{
|
||||
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>();
|
||||
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
|
||||
{
|
||||
public IMessageContext MessageContext { get; set; }
|
||||
public IControllerMessageContext MessageContext { get; set; }
|
||||
public IFlowStateLock FlowStateLock { 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client.Framing;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
using Tapeti.Flow.Annotations;
|
||||
using Tapeti.Flow.FlowHelpers;
|
||||
|
||||
@ -14,11 +14,11 @@ namespace Tapeti.Flow.Default
|
||||
{
|
||||
public class FlowProvider : IFlowProvider, IFlowHandler
|
||||
{
|
||||
private readonly IConfig config;
|
||||
private readonly ITapetiConfig config;
|
||||
private readonly IInternalPublisher publisher;
|
||||
|
||||
|
||||
public FlowProvider(IConfig config, IPublisher publisher)
|
||||
public FlowProvider(ITapetiConfig config, IPublisher publisher)
|
||||
{
|
||||
this.config = config;
|
||||
this.publisher = (IInternalPublisher)publisher;
|
||||
@ -72,7 +72,7 @@ namespace Tapeti.Flow.Default
|
||||
ConvergeMethodSync = convergeMethodTaskSync
|
||||
});
|
||||
|
||||
var properties = new BasicProperties
|
||||
var properties = new MessageProperties
|
||||
{
|
||||
CorrelationId = continuationID.ToString(),
|
||||
ReplyTo = responseHandlerInfo.ReplyToQueue
|
||||
@ -96,12 +96,10 @@ namespace Tapeti.Flow.Default
|
||||
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");
|
||||
|
||||
var properties = new BasicProperties();
|
||||
|
||||
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
||||
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
||||
if (reply.CorrelationId != null)
|
||||
properties.CorrelationId = reply.CorrelationId;
|
||||
var properties = new MessageProperties
|
||||
{
|
||||
CorrelationId = reply.CorrelationId
|
||||
};
|
||||
|
||||
// TODO disallow if replyto is not specified?
|
||||
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)
|
||||
throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler));
|
||||
|
||||
@ -158,13 +156,13 @@ namespace Tapeti.Flow.Default
|
||||
CorrelationId = context.Properties.CorrelationId,
|
||||
ReplyTo = context.Properties.ReplyTo,
|
||||
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();
|
||||
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))
|
||||
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Binding.Method.Name}");
|
||||
|
||||
FlowContext flowContext;
|
||||
|
||||
if (!context.Items.TryGetValue(ContextItems.FlowContext, out var flowContextItem))
|
||||
if (!context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||
{
|
||||
flowContext = new FlowContext
|
||||
{
|
||||
MessageContext = context
|
||||
};
|
||||
|
||||
context.Items.Add(ContextItems.FlowContext, flowContext);
|
||||
context.Store(ContextItems.FlowContext, flowContext);
|
||||
}
|
||||
else
|
||||
flowContext = (FlowContext)flowContextItem;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
@ -234,12 +227,12 @@ namespace Tapeti.Flow.Default
|
||||
}
|
||||
|
||||
|
||||
private readonly IConfig config;
|
||||
private readonly ITapetiConfig config;
|
||||
private readonly SendRequestFunc sendRequest;
|
||||
private readonly List<RequestInfo> requests = new List<RequestInfo>();
|
||||
|
||||
|
||||
public ParallelRequestBuilder(IConfig config, SendRequestFunc sendRequest)
|
||||
public ParallelRequestBuilder(ITapetiConfig config, SendRequestFunc sendRequest)
|
||||
{
|
||||
this.config = config;
|
||||
this.sendRequest = sendRequest;
|
||||
|
@ -9,11 +9,11 @@ namespace Tapeti.Flow.Default
|
||||
{
|
||||
public class FlowStarter : IFlowStarter
|
||||
{
|
||||
private readonly IConfig config;
|
||||
private readonly ITapetiConfig config;
|
||||
private readonly ILogger logger;
|
||||
|
||||
|
||||
public FlowStarter(IConfig config, ILogger logger)
|
||||
public FlowStarter(ITapetiConfig config, ILogger logger)
|
||||
{
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
@ -47,9 +47,9 @@ namespace Tapeti.Flow.Default
|
||||
var controller = config.DependencyResolver.Resolve<TController>();
|
||||
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
|
||||
|
||||
var context = new MessageContext
|
||||
var context = new ControllerMessageContext
|
||||
{
|
||||
DependencyResolver = config.DependencyResolver,
|
||||
Config = config,
|
||||
Controller = controller
|
||||
};
|
||||
|
||||
@ -72,6 +72,7 @@ namespace Tapeti.Flow.Default
|
||||
|
||||
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
||||
{
|
||||
/*
|
||||
foreach (var handler in config.CleanupMiddleware)
|
||||
{
|
||||
try
|
||||
@ -83,6 +84,7 @@ namespace Tapeti.Flow.Default
|
||||
logger.HandlerException(eCleanup);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,7 +25,6 @@ namespace Tapeti.Flow
|
||||
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
|
||||
{
|
||||
yield return new FlowBindingMiddleware();
|
||||
yield return new FlowCleanupMiddleware();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace Tapeti.Flow
|
||||
/// </summary>
|
||||
public interface IFlowHandler
|
||||
{
|
||||
Task Execute(IMessageContext context, IYieldPoint yieldPoint);
|
||||
Task Execute(IControllerMessageContext context, IYieldPoint yieldPoint);
|
||||
}
|
||||
|
||||
public interface IFlowParallelRequestBuilder
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tapeti.Annotations\Tapeti.Annotations.csproj" />
|
||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
</ItemGroup>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SimpleInjector" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
@ -2,7 +2,7 @@
|
||||
using Tapeti.Default;
|
||||
using Xunit;
|
||||
|
||||
namespace Tapeti.Tests
|
||||
namespace Tapeti.Tests.Default
|
||||
{
|
||||
// ReSharper disable InconsistentNaming
|
||||
public class TypeNameRoutingKeyStrategyTests
|
@ -1,9 +1,8 @@
|
||||
using Tapeti.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Tapeti.Tests
|
||||
namespace Tapeti.Tests.Helpers
|
||||
{
|
||||
// ReSharper disable InconsistentNaming
|
||||
public class ConnectionStringParserTest
|
||||
{
|
||||
[Fact]
|
@ -4,6 +4,10 @@
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
@ -14,4 +18,8 @@
|
||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Core\" />
|
||||
</ItemGroup>
|
||||
|
||||
</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 Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Transient
|
||||
{
|
||||
/// <summary>
|
||||
/// TapetiConfig extension to register Tapeti.Transient
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,17 @@
|
||||
|
||||
namespace Tapeti.Transient
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a publisher which can send request messages, and await the response on a dynamic queue.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tapeti\Tapeti.csproj" />
|
||||
</ItemGroup>
|
||||
|
@ -4,29 +4,38 @@ using Tapeti.Config;
|
||||
|
||||
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;
|
||||
|
||||
public TransientMiddleware(TimeSpan defaultTimeout, string dynamicQueuePrefix)
|
||||
|
||||
/// <inheritdoc />
|
||||
public TransientExtension(TimeSpan defaultTimeout, string dynamicQueuePrefix)
|
||||
{
|
||||
this.dynamicQueuePrefix = dynamicQueuePrefix;
|
||||
this.router = new TransientRouter(defaultTimeout);
|
||||
router = new TransientRouter(defaultTimeout);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterDefaults(IDependencyContainer container)
|
||||
{
|
||||
container.RegisterDefaultSingleton(router);
|
||||
container.RegisterDefault<ITransientPublisher, TransientPublisher>();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
@ -1,52 +1,51 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Config;
|
||||
|
||||
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 string dynamicQueuePrefix;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string QueueName { get; private set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TransientGenericBinding(TransientRouter router, string dynamicQueuePrefix)
|
||||
{
|
||||
this.router = router;
|
||||
DynamicQueuePrefix = dynamicQueuePrefix;
|
||||
Method = typeof(TransientRouter).GetMethod("GenericHandleResponse");
|
||||
this.dynamicQueuePrefix = dynamicQueuePrefix;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public void SetQueueName(string queueName)
|
||||
{
|
||||
router.TransientResponseQueueName = queueName;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Transient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Default implementation of ITransientPublisher
|
||||
/// </summary>
|
||||
public class TransientPublisher : ITransientPublisher
|
||||
{
|
||||
private readonly TransientRouter router;
|
||||
private readonly IPublisher publisher;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TransientPublisher(TransientRouter router, IPublisher publisher)
|
||||
{
|
||||
this.router = router;
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request)
|
||||
{
|
||||
return (TResponse)(await router.RequestResponse(publisher, request));
|
||||
|
@ -2,26 +2,37 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using RabbitMQ.Client.Framing;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
|
||||
namespace Tapeti.Transient
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages active requests and responses. For internal use.
|
||||
/// </summary>
|
||||
public class TransientRouter
|
||||
{
|
||||
private readonly int defaultTimeoutMs;
|
||||
|
||||
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; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public TransientRouter(TimeSpan defaultTimeout)
|
||||
{
|
||||
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)
|
||||
return;
|
||||
@ -30,9 +41,16 @@ namespace Tapeti.Transient
|
||||
return;
|
||||
|
||||
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)
|
||||
{
|
||||
var correlation = Guid.NewGuid();
|
||||
@ -40,7 +58,7 @@ namespace Tapeti.Transient
|
||||
|
||||
try
|
||||
{
|
||||
var properties = new BasicProperties
|
||||
var properties = new MessageProperties
|
||||
{
|
||||
CorrelationId = correlation.ToString(),
|
||||
ReplyTo = TransientResponseQueueName,
|
||||
@ -64,6 +82,7 @@ namespace Tapeti.Transient
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void TimeoutResponse(object tcs)
|
||||
{
|
||||
((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">
|
||||
<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/=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.Collections.Generic;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Provides information about the message currently being handled.
|
||||
/// </summary>
|
||||
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 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>
|
||||
/// Controller will be null when passed to a IMessageFilterMiddleware
|
||||
/// Provides access to the binding which is currently processing the message.
|
||||
/// </remarks>
|
||||
object Controller { 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes middleware that processes all messages.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to information about the message being published.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// The instance of the message class.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <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
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows the extension to register default implementations into the IoC container.
|
||||
/// </summary>
|
||||
/// <param name="container"></param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
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
|
||||
{
|
||||
public class DisconnectedEventArgs
|
||||
{
|
||||
public ushort ReplyCode;
|
||||
public string ReplyText;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Receives notifications on the state of the connection.
|
||||
/// </summary>
|
||||
public interface IConnectionEventListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a connection to RabbitMQ has been established.
|
||||
/// </summary>
|
||||
void Connected();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection to RabbitMQ has been lost.
|
||||
/// </summary>
|
||||
void Reconnected();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection to RabbitMQ has been recovered after an unexpected disconnect.
|
||||
/// </summary>
|
||||
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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using RabbitMQ.Client.Exceptions;
|
||||
using RabbitMQ.Client.Framing;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
using Tapeti.Exceptions;
|
||||
using Tapeti.Helpers;
|
||||
using Tapeti.Tasks;
|
||||
|
||||
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 MandatoryReturnTimeout = 30000;
|
||||
private const int MinimumConnectedReconnectDelay = 1000;
|
||||
|
||||
private readonly IConfig config;
|
||||
private readonly TapetiConnectionParams connectionParams;
|
||||
|
||||
private readonly ITapetiConfig config;
|
||||
private readonly ILogger logger;
|
||||
public TapetiConnectionParams ConnectionParams { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Receives events when the connection state changes.
|
||||
/// </summary>
|
||||
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>();
|
||||
|
||||
|
||||
@ -39,6 +50,7 @@ namespace Tapeti.Connection
|
||||
private IModel channelInstance;
|
||||
private ulong lastDeliveryTag;
|
||||
private DateTime connectedDateTime;
|
||||
private HttpClient managementClient;
|
||||
|
||||
// These fields must be locked, since the callbacks for BasicAck/BasicReturn can run in a different thread
|
||||
private readonly object confirmLock = new object();
|
||||
@ -60,88 +72,123 @@ namespace Tapeti.Connection
|
||||
}
|
||||
|
||||
|
||||
|
||||
public TapetiWorker(IConfig config)
|
||||
/// <inheritdoc />
|
||||
public TapetiClient(ITapetiConfig config, TapetiConnectionParams connectionParams)
|
||||
{
|
||||
this.config = config;
|
||||
this.connectionParams = connectionParams;
|
||||
|
||||
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)
|
||||
{
|
||||
return Publish(message, properties, exchangeStrategy.GetExchange(message.GetType()), routingKeyStrategy.GetRoutingKey(message.GetType()), mandatory);
|
||||
}
|
||||
|
||||
|
||||
public Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory)
|
||||
{
|
||||
return Publish(message, properties, "", queueName, mandatory);
|
||||
}
|
||||
|
||||
|
||||
public Task Consume(string queueName, IEnumerable<IBinding> bindings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(queueName))
|
||||
throw new ArgumentNullException(nameof(queueName));
|
||||
|
||||
return taskQueue.Value.Add(() =>
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
WithRetryableChannel(channel => channel.BasicConsume(queueName, false, new TapetiConsumer(this, queueName, config.DependencyResolver, bindings, config.MessageMiddleware, config.CleanupMiddleware)));
|
||||
Credentials = new NetworkCredential(connectionParams.Username, connectionParams.Password)
|
||||
};
|
||||
|
||||
managementClient = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
managementClient.DefaultRequestHeaders.Add("Connection", "close");
|
||||
}
|
||||
|
||||
|
||||
/// <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}");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public Task Subscribe(IQueue queue)
|
||||
/// <inheritdoc />
|
||||
public async Task Consume(string queueName, IConsumer consumer)
|
||||
{
|
||||
return taskQueue.Value.Add(() =>
|
||||
if (string.IsNullOrEmpty(queueName))
|
||||
throw new ArgumentNullException(nameof(queueName));
|
||||
|
||||
await taskQueue.Value.Add(() =>
|
||||
{
|
||||
WithRetryableChannel(channel =>
|
||||
WithRetryableChannel(channel =>
|
||||
{
|
||||
if (queue.Dynamic)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
var basicConsumer = new TapetiBasicConsumer(consumer, Respond);
|
||||
channel.BasicConsume(queueName, false, basicConsumer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
// 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)
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
|
||||
return taskQueue.Value.Add(() =>
|
||||
await taskQueue.Value.Add(() =>
|
||||
{
|
||||
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
|
||||
{
|
||||
DependencyResolver = config.DependencyResolver,
|
||||
Exchange = exchange,
|
||||
RoutingKey = routingKey,
|
||||
Message = message,
|
||||
Properties = properties ?? new BasicProperties()
|
||||
};
|
||||
HttpStatusCode.GatewayTimeout,
|
||||
HttpStatusCode.RequestTimeout,
|
||||
HttpStatusCode.ServiceUnavailable
|
||||
};
|
||||
|
||||
if (!context.Properties.IsTimestampPresent())
|
||||
context.Properties.Timestamp = new AmqpTimestamp(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds());
|
||||
|
||||
if (!context.Properties.IsDeliveryModePresent())
|
||||
context.Properties.DeliveryMode = 2; // Persistent
|
||||
private static readonly TimeSpan[] ExponentialBackoff =
|
||||
{
|
||||
TimeSpan.FromSeconds(1),
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(3),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(8),
|
||||
TimeSpan.FromSeconds(13),
|
||||
TimeSpan.FromSeconds(21),
|
||||
TimeSpan.FromSeconds(34),
|
||||
TimeSpan.FromSeconds(55)
|
||||
};
|
||||
|
||||
|
||||
// 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 () =>
|
||||
private class ManagementBinding
|
||||
{
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
|
||||
[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)
|
||||
{
|
||||
if (Thread.CurrentThread.ManagedThreadId != 3)
|
||||
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
var body = messageSerializer.Serialize(context.Message, context.Properties);
|
||||
|
||||
Task<int> publishResultTask = null;
|
||||
var messageInfo = new ConfirmMessageInfo
|
||||
try
|
||||
{
|
||||
ReturnKey = GetReturnKey(context.Exchange, context.RoutingKey),
|
||||
CompletionSource = new TaskCompletionSource<int>()
|
||||
};
|
||||
var response = await managementClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var bindings = JsonConvert.DeserializeObject<IEnumerable<ManagementBinding>>(content);
|
||||
|
||||
WithRetryableChannel(channel =>
|
||||
// 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));
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
// The delivery tag is lost after a reconnect, register under the new tag
|
||||
if (config.UsePublisherConfirms)
|
||||
{
|
||||
lastDeliveryTag++;
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
if (!(e.Response is HttpWebResponse response))
|
||||
throw;
|
||||
|
||||
Monitor.Enter(confirmLock);
|
||||
try
|
||||
{
|
||||
confirmMessages.Add(lastDeliveryTag, messageInfo);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(confirmLock);
|
||||
}
|
||||
if (!TransientStatusCodes.Contains(response.StatusCode))
|
||||
throw;
|
||||
}
|
||||
|
||||
publishResultTask = messageInfo.CompletionSource.Task;
|
||||
}
|
||||
else
|
||||
mandatory = false;
|
||||
await Task.Delay(ExponentialBackoff[retryDelayIndex]);
|
||||
|
||||
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
|
||||
if (retryDelayIndex < ExponentialBackoff.Length - 1)
|
||||
retryDelayIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -298,9 +416,8 @@ namespace Tapeti.Connection
|
||||
operation(GetChannel());
|
||||
break;
|
||||
}
|
||||
catch (AlreadyClosedException e)
|
||||
catch (AlreadyClosedException)
|
||||
{
|
||||
// TODO log?
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -323,11 +440,11 @@ namespace Tapeti.Connection
|
||||
|
||||
var connectionFactory = new ConnectionFactory
|
||||
{
|
||||
HostName = ConnectionParams.HostName,
|
||||
Port = ConnectionParams.Port,
|
||||
VirtualHost = ConnectionParams.VirtualHost,
|
||||
UserName = ConnectionParams.Username,
|
||||
Password = ConnectionParams.Password,
|
||||
HostName = connectionParams.HostName,
|
||||
Port = connectionParams.Port,
|
||||
VirtualHost = connectionParams.VirtualHost,
|
||||
UserName = connectionParams.Username,
|
||||
Password = connectionParams.Password,
|
||||
AutomaticRecoveryEnabled = false,
|
||||
TopologyRecoveryEnabled = false,
|
||||
RequestedHeartbeat = 30
|
||||
@ -337,7 +454,7 @@ namespace Tapeti.Connection
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Connect(ConnectionParams);
|
||||
logger.Connect(connectionParams);
|
||||
|
||||
connection = connectionFactory.CreateConnection();
|
||||
channelInstance = connection.CreateModel();
|
||||
@ -345,13 +462,16 @@ namespace Tapeti.Connection
|
||||
if (channelInstance == null)
|
||||
throw new BrokerUnreachableException(null);
|
||||
|
||||
if (config.UsePublisherConfirms)
|
||||
if (config.Features.PublisherConfirms)
|
||||
{
|
||||
lastDeliveryTag = 0;
|
||||
|
||||
Monitor.Enter(confirmLock);
|
||||
try
|
||||
{
|
||||
foreach (var pair in confirmMessages)
|
||||
pair.Value.CompletionSource.SetCanceled();
|
||||
|
||||
confirmMessages.Clear();
|
||||
}
|
||||
finally
|
||||
@ -362,8 +482,8 @@ namespace Tapeti.Connection
|
||||
channelInstance.ConfirmSelect();
|
||||
}
|
||||
|
||||
if (ConnectionParams.PrefetchCount > 0)
|
||||
channelInstance.BasicQos(0, ConnectionParams.PrefetchCount, false);
|
||||
if (connectionParams.PrefetchCount > 0)
|
||||
channelInstance.BasicQos(0, connectionParams.PrefetchCount, false);
|
||||
|
||||
channelInstance.ModelShutdown += (sender, e) =>
|
||||
{
|
||||
@ -390,14 +510,14 @@ namespace Tapeti.Connection
|
||||
else
|
||||
ConnectionEventListener?.Connected();
|
||||
|
||||
logger.ConnectSuccess(ConnectionParams);
|
||||
logger.ConnectSuccess(connectionParams);
|
||||
isReconnect = true;
|
||||
|
||||
break;
|
||||
}
|
||||
catch (BrokerUnreachableException e)
|
||||
{
|
||||
logger.ConnectFailed(ConnectionParams, e);
|
||||
logger.ConnectFailed(connectionParams, e);
|
||||
Thread.Sleep(ReconnectDelay);
|
||||
}
|
||||
}
|
||||
@ -507,15 +627,5 @@ namespace Tapeti.Connection
|
||||
{
|
||||
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,309 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Helpers;
|
||||
|
||||
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 IDependencyResolver dependencyResolver;
|
||||
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
||||
private readonly IReadOnlyList<ICleanupMiddleware> cleanupMiddleware;
|
||||
private readonly List<IBinding> bindings;
|
||||
|
||||
private readonly ILogger logger;
|
||||
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.dependencyResolver = dependencyResolver;
|
||||
this.messageMiddleware = messageMiddleware;
|
||||
this.cleanupMiddleware = cleanupMiddleware;
|
||||
this.bindings = bindings.ToList();
|
||||
|
||||
logger = dependencyResolver.Resolve<ILogger>();
|
||||
exceptionStrategy = dependencyResolver.Resolve<IExceptionStrategy>();
|
||||
logger = config.DependencyResolver.Resolve<ILogger>();
|
||||
exceptionStrategy = config.DependencyResolver.Resolve<IExceptionStrategy>();
|
||||
messageSerializer = config.DependencyResolver.Resolve<IMessageSerializer>();
|
||||
}
|
||||
|
||||
|
||||
public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey,
|
||||
IBasicProperties properties, byte[] body)
|
||||
/// <inheritdoc />
|
||||
public async Task<ConsumeResponse> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
try
|
||||
{
|
||||
MessageContext context = null;
|
||||
HandlingResult handlingResult = null;
|
||||
try
|
||||
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
|
||||
{
|
||||
Exchange = exchange,
|
||||
RoutingKey = routingKey,
|
||||
Properties = properties
|
||||
});
|
||||
|
||||
return ConsumeResponse.Ack;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO exception strategy
|
||||
// TODO logger
|
||||
return ConsumeResponse.Nack;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Ack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
catch (Exception eDispatch)
|
||||
{
|
||||
var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch));
|
||||
logger.HandlerException(eDispatch);
|
||||
try
|
||||
{
|
||||
context = new MessageContext
|
||||
{
|
||||
DependencyResolver = dependencyResolver,
|
||||
Queue = queueName,
|
||||
RoutingKey = routingKey,
|
||||
Properties = properties
|
||||
};
|
||||
var exceptionStrategyContext = new ExceptionStrategyContext(context, exception.SourceException);
|
||||
|
||||
await DispatchMesage(context, body);
|
||||
exceptionStrategy.HandleException(exceptionStrategyContext);
|
||||
|
||||
handlingResult = exceptionStrategyContext.HandlingResult.ToHandlingResult();
|
||||
}
|
||||
catch (Exception eStrategy)
|
||||
{
|
||||
logger.HandlerException(eStrategy);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (handlingResult == null)
|
||||
{
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Ack,
|
||||
ConsumeResponse = ConsumeResponse.Nack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
catch (Exception eDispatch)
|
||||
{
|
||||
var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch));
|
||||
logger.HandlerException(eDispatch);
|
||||
try
|
||||
{
|
||||
var exceptionStrategyContext = new ExceptionStrategyContext(context, exception.SourceException);
|
||||
|
||||
exceptionStrategy.HandleException(exceptionStrategyContext);
|
||||
|
||||
handlingResult = exceptionStrategyContext.HandlingResult.ToHandlingResult();
|
||||
}
|
||||
catch (Exception eStrategy)
|
||||
{
|
||||
logger.HandlerException(eStrategy);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (handlingResult == null)
|
||||
{
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Nack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
await RunCleanup(context, handlingResult);
|
||||
}
|
||||
catch (Exception eCleanup)
|
||||
{
|
||||
logger.HandlerException(eCleanup);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (handlingResult == null)
|
||||
{
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Nack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
await worker.Respond(deliveryTag, handlingResult.ConsumeResponse);
|
||||
}
|
||||
catch (Exception eRespond)
|
||||
{
|
||||
logger.HandlerException(eRespond);
|
||||
}
|
||||
try
|
||||
{
|
||||
context?.Dispose();
|
||||
}
|
||||
catch (Exception eDispose)
|
||||
{
|
||||
logger.HandlerException(eDispose);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
||||
{
|
||||
foreach(var handler in cleanupMiddleware)
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.Handle(context, handlingResult);
|
||||
await RunCleanup(context, handlingResult);
|
||||
}
|
||||
catch (Exception eCleanup)
|
||||
{
|
||||
logger.HandlerException(eCleanup);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (handlingResult == null)
|
||||
{
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Nack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
await client.Respond(deliveryTag, handlingResult.ConsumeResponse);
|
||||
}
|
||||
catch (Exception eRespond)
|
||||
{
|
||||
logger.HandlerException(eRespond);
|
||||
}
|
||||
try
|
||||
{
|
||||
context?.Dispose();
|
||||
}
|
||||
catch (Exception eDispose)
|
||||
{
|
||||
logger.HandlerException(eDispose);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
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);
|
||||
if (message == null)
|
||||
throw new ArgumentException("Empty message");
|
||||
|
||||
context.Message = message;
|
||||
|
||||
var messageType = message.GetType();
|
||||
var validMessageType = false;
|
||||
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.Accept(context, message))
|
||||
{
|
||||
await InvokeUsingBinding(context, binding, message);
|
||||
if (!binding.Accept(messageType))
|
||||
continue;
|
||||
|
||||
validMessageType = true;
|
||||
}
|
||||
await InvokeUsingBinding(message, messageContextData, binding);
|
||||
validMessageType = true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
RecursiveCaller firstCaller = null;
|
||||
RecursiveCaller currentCaller = null;
|
||||
|
||||
void AddHandler(Handler handle)
|
||||
var context = new MessageContext
|
||||
{
|
||||
var caller = new RecursiveCaller(handle);
|
||||
if (currentCaller == null)
|
||||
firstCaller = caller;
|
||||
else
|
||||
currentCaller.Next = caller;
|
||||
currentCaller = caller;
|
||||
}
|
||||
|
||||
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.");
|
||||
Config = config,
|
||||
Queue = queueName,
|
||||
Exchange = messageContextData.Exchange,
|
||||
RoutingKey = messageContextData.RoutingKey,
|
||||
Message = message,
|
||||
Properties = messageContextData.Properties,
|
||||
Binding = binding
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
currentContext = context;
|
||||
|
||||
context.UseNestedContext = Next == null ? (Action<MessageContext>)null : UseNestedContext;
|
||||
|
||||
await handle(context, CallNext);
|
||||
await MiddlewareHelper.GoAsync(config.Middleware.Message,
|
||||
(handler, next) => handler.Handle(context, next),
|
||||
async () => { await binding.Invoke(context); });
|
||||
}
|
||||
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)
|
||||
throw new InvalidOperationException("Previous nested context was not yet disposed.");
|
||||
|
||||
context.OnContextDisposed = OnContextDisposed;
|
||||
nextContext = context;
|
||||
}
|
||||
|
||||
void OnContextDisposed(MessageContext context)
|
||||
{
|
||||
context.OnContextDisposed = null;
|
||||
if (nextContext == context)
|
||||
nextContext = null;
|
||||
public string Exchange;
|
||||
public string RoutingKey;
|
||||
public IMessageProperties Properties;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,37 +1,92 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
using Tapeti.Exceptions;
|
||||
using Tapeti.Helpers;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class TapetiSubscriber : ISubscriber
|
||||
{
|
||||
private readonly Func<TapetiWorker> workerFactory;
|
||||
private readonly List<IQueue> queues;
|
||||
private readonly Func<ITapetiClient> clientFactory;
|
||||
private readonly ITapetiConfig config;
|
||||
private bool consuming;
|
||||
|
||||
|
||||
public TapetiSubscriber(Func<TapetiWorker> workerFactory, IEnumerable<IQueue> queues)
|
||||
/// <inheritdoc />
|
||||
public TapetiSubscriber(Func<ITapetiClient> clientFactory, ITapetiConfig config)
|
||||
{
|
||||
this.workerFactory = workerFactory;
|
||||
this.queues = queues.ToList();
|
||||
this.clientFactory = clientFactory;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
||||
public Task BindQueues()
|
||||
{
|
||||
return Task.WhenAll(queues.Select(queue => workerFactory().Subscribe(queue)).ToList());
|
||||
}
|
||||
|
||||
|
||||
public Task RebindQueues()
|
||||
/// <summary>
|
||||
/// Applies the configured bindings and declares the queues in RabbitMQ. For internal use only.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task ApplyBindings()
|
||||
{
|
||||
return BindQueues();
|
||||
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 Resume()
|
||||
/// <inheritdoc />
|
||||
public async Task Resume()
|
||||
{
|
||||
if (consuming)
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the response sent back after handling a message.
|
||||
/// </summary>
|
||||
public enum ConsumeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Acknowledge the message and remove it from the queue
|
||||
/// </summary>
|
||||
Ack,
|
||||
|
||||
/// <summary>
|
||||
/// Negatively acknowledge the message and remove it from the queue, send to dead-letter queue if configured on the bus
|
||||
/// </summary>
|
||||
Nack,
|
||||
|
||||
/// <summary>
|
||||
/// Negatively acknowledge the message and put it back in the queue to try again later
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
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();
|
||||
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <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.
|
||||
/// 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
|
||||
/// enum and therefore skips the naming strategy.
|
||||
/// </summary>
|
||||
@ -17,12 +17,14 @@ namespace Tapeti.Default
|
||||
private readonly int invalidEnumValue;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public FallbackStringEnumConverter()
|
||||
{
|
||||
unchecked { invalidEnumValue = (int)0xDEADBEEF; }
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
@ -39,6 +41,7 @@ namespace Tapeti.Default
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var isNullable = IsNullableType(objectType);
|
||||
@ -72,6 +75,7 @@ namespace Tapeti.Default
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
var actualType = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;
|
||||
|
@ -1,22 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// IMessageSerializer implementation for JSON encoding and decoding using Newtonsoft.Json.
|
||||
/// </summary>
|
||||
public class JsonMessageSerializer : IMessageSerializer
|
||||
{
|
||||
protected const string ContentType = "application/json";
|
||||
protected const string ClassTypeHeader = "classType";
|
||||
private const string ContentType = "application/json";
|
||||
private const string ClassTypeHeader = "classType";
|
||||
|
||||
|
||||
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>();
|
||||
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
||||
private readonly JsonSerializerSettings serializerSettings;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public JsonMessageSerializer()
|
||||
{
|
||||
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);
|
||||
|
||||
properties.Headers.Add(ClassTypeHeader, Encoding.UTF8.GetBytes(typeName));
|
||||
properties.SetHeader(ClassTypeHeader, typeName);
|
||||
properties.ContentType = ContentType;
|
||||
|
||||
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))
|
||||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
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(':');
|
||||
if (parts.Length != 2)
|
||||
@ -69,7 +80,14 @@ namespace Tapeti.Default
|
||||
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;
|
||||
if (typeName.Length > 255)
|
||||
|
@ -3,9 +3,15 @@ using Tapeti.Config;
|
||||
|
||||
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)
|
||||
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");
|
||||
|
||||
parameter.SetBinding(messageContext => messageContext.Message);
|
||||
context.MessageClass = parameter.Info.ParameterType;
|
||||
context.SetMessageClass(parameter.Info.ParameterType);
|
||||
|
||||
next();
|
||||
}
|
||||
|
@ -1,172 +1,34 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
using System.Linq;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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 string Queue { get; set; }
|
||||
public string RoutingKey { get; set; }
|
||||
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()
|
||||
/// <inheritdoc />
|
||||
public virtual void Dispose()
|
||||
{
|
||||
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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client.Framing;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Helpers;
|
||||
|
||||
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();
|
||||
|
||||
@ -60,18 +64,15 @@ namespace Tapeti.Default
|
||||
if (message == null)
|
||||
throw new ArgumentException("Return value of a request message handler must not be null");
|
||||
|
||||
var publisher = (IInternalPublisher)messageContext.DependencyResolver.Resolve<IPublisher>();
|
||||
var properties = new BasicProperties();
|
||||
var publisher = (IInternalPublisher)messageContext.Config.DependencyResolver.Resolve<IPublisher>();
|
||||
var properties = new MessageProperties
|
||||
{
|
||||
CorrelationId = messageContext.Properties.CorrelationId
|
||||
};
|
||||
|
||||
// Only set the property if it's not null, otherwise a string reference exception can occur:
|
||||
// http://rabbitmq.1065348.n5.nabble.com/SocketException-when-invoking-model-BasicPublish-td36330.html
|
||||
if (messageContext.Properties.IsCorrelationIdPresent())
|
||||
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);
|
||||
return !string.IsNullOrEmpty(messageContext.Properties.ReplyTo)
|
||||
? publisher.PublishDirect(message, messageContext.Properties.ReplyTo, properties, messageContext.Properties.Persistent.GetValueOrDefault(true))
|
||||
: 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to construct a TapetiConnectionParams instance based on the
|
||||
/// ConnectionString syntax as used by EasyNetQ.
|
||||
/// </summary>
|
||||
public class ConnectionStringParser
|
||||
{
|
||||
private readonly TapetiConnectionParams result = new TapetiConnectionParams();
|
||||
@ -10,6 +14,10 @@ namespace Tapeti.Helpers
|
||||
private int pos = -1;
|
||||
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)
|
||||
{
|
||||
return new ConnectionStringParser(connectionstring).result;
|
||||
@ -106,7 +114,9 @@ namespace Tapeti.Helpers
|
||||
|
||||
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 "port": result.Port = int.Parse(value); break;
|
||||
case "virtualhost": result.VirtualHost = value; break;
|
||||
|
@ -5,8 +5,76 @@ using System.Threading.Tasks;
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper interface for an IoC container to allow dependency injection in Tapeti.
|
||||
/// </summary>
|
||||
public interface IDependencyResolver
|
||||
{
|
||||
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
|
||||
{
|
||||
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||
|
@ -1,10 +1,26 @@
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides serialization and deserialization for messages.
|
||||
/// </summary>
|
||||
public interface IMessageSerializer
|
||||
{
|
||||
byte[] Serialize(object message, IBasicProperties properties);
|
||||
object Deserialize(byte[] body, IBasicProperties properties);
|
||||
/// <summary>
|
||||
/// 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 RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
// Note: 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.
|
||||
/// <summary>
|
||||
/// Allows publishing of messages.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/// <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
|
||||
{
|
||||
Task Publish(object message, IBasicProperties properties, bool mandatory);
|
||||
Task PublishDirect(object message, string queueName, IBasicProperties properties, bool mandatory);
|
||||
/// <summary>
|
||||
/// Publishes a message. The exchange and routing key are determined by the registered strategies.
|
||||
/// </summary>
|
||||
/// <param name="message">An instance of a message class</param>
|
||||
/// <param name="properties">Metadata to include in the message</param>
|
||||
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param>
|
||||
Task Publish(object message, IMessageProperties properties, bool mandatory);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message directly to a queue. The exchange and routing key are not used.
|
||||
/// </summary>
|
||||
/// <param name="message">An instance of a message class</param>
|
||||
/// <param name="queueName">The name of the queue to send the message to</param>
|
||||
/// <param name="properties">Metadata to include in the message</param>
|
||||
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to the queue</param>
|
||||
/// <returns></returns>
|
||||
Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory);
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,14 @@
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages subscriptions to queues as configured by the bindings.
|
||||
/// </summary>
|
||||
public interface ISubscriber
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts consuming from the subscribed queues if not already started.
|
||||
/// </summary>
|
||||
Task Resume();
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" />
|
||||
|
@ -2,8 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Default;
|
||||
using Tapeti.Helpers;
|
||||
@ -12,198 +10,169 @@ using Tapeti.Helpers;
|
||||
|
||||
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) { }
|
||||
}
|
||||
|
||||
public delegate Task MessageHandlerFunc(IMessageContext context, object message);
|
||||
private Config config;
|
||||
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new List<IControllerBindingMiddleware>();
|
||||
|
||||
|
||||
public class TapetiConfig
|
||||
{
|
||||
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;
|
||||
/// <inheritdoc />
|
||||
public IDependencyResolver DependencyResolver => GetConfig().DependencyResolver;
|
||||
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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 MessageBinding());
|
||||
Use(new PublishResultBinding());
|
||||
}
|
||||
|
||||
|
||||
public IConfig Build()
|
||||
{
|
||||
RegisterCustomBindings();
|
||||
// Registered last so it runs first and the MessageClass is known to other middleware
|
||||
Use(new MessageBinding());
|
||||
|
||||
RegisterDefaults();
|
||||
|
||||
var queues = new List<IQueue>();
|
||||
queues.AddRange(staticRegistrations.Select(qb => new Queue(new QueueInfo { Dynamic = false, Name = qb.Key }, qb.Value)));
|
||||
(config.DependencyResolver as IDependencyContainer)?.RegisterDefaultSingleton<ITapetiConfig>(config);
|
||||
|
||||
|
||||
// We want to ensure each queue only has unique messages classes. This means we can requeue
|
||||
// without the side-effect of calling other handlers for the same message class again as well.
|
||||
//
|
||||
// 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>>();
|
||||
var outputConfig = config;
|
||||
config = null;
|
||||
|
||||
foreach (var bindings in prefixGroup.Value.Values)
|
||||
{
|
||||
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;
|
||||
outputConfig.Lock();
|
||||
return outputConfig;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IBindingMiddleware handler)
|
||||
/// <inheritdoc />
|
||||
public ITapetiConfigBuilder Use(IControllerBindingMiddleware handler)
|
||||
{
|
||||
bindingMiddleware.Add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IMessageMiddleware handler)
|
||||
/// <inheritdoc />
|
||||
public ITapetiConfigBuilder Use(IMessageMiddleware handler)
|
||||
{
|
||||
messageMiddleware.Add(handler);
|
||||
GetConfig().Use(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(ICleanupMiddleware handler)
|
||||
/// <inheritdoc />
|
||||
public ITapetiConfigBuilder Use(IPublishMiddleware handler)
|
||||
{
|
||||
cleanupMiddleware.Add(handler);
|
||||
GetConfig().Use(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IPublishMiddleware handler)
|
||||
/// <inheritdoc />
|
||||
public ITapetiConfigBuilder Use(ITapetiExtension extension)
|
||||
{
|
||||
publishMiddleware.Add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(ITapetiExtension extension)
|
||||
{
|
||||
if (dependencyResolver is IDependencyContainer container)
|
||||
if (DependencyResolver is IDependencyContainer container)
|
||||
extension.RegisterDefaults(container);
|
||||
|
||||
var middlewareBundle = extension.GetMiddleware(dependencyResolver);
|
||||
var configInstance = GetConfig();
|
||||
|
||||
if (extension is ITapetiExtentionBinding extentionBindings)
|
||||
customBindings.AddRange(extentionBindings.GetBindings(dependencyResolver));
|
||||
|
||||
// ReSharper disable once InvertIf
|
||||
var middlewareBundle = extension.GetMiddleware(DependencyResolver);
|
||||
if (middlewareBundle != null)
|
||||
{
|
||||
foreach (var middleware in middlewareBundle)
|
||||
{
|
||||
// ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
|
||||
if (middleware is IBindingMiddleware bindingExtension)
|
||||
Use(bindingExtension);
|
||||
else if (middleware is IMessageMiddleware messageExtension)
|
||||
Use(messageExtension);
|
||||
else if (middleware is ICleanupMiddleware cleanupExtension)
|
||||
Use(cleanupExtension);
|
||||
else if (middleware is IPublishMiddleware publishExtension)
|
||||
Use(publishExtension);
|
||||
else
|
||||
throw new ArgumentException($"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}");
|
||||
switch (middleware)
|
||||
{
|
||||
case IControllerBindingMiddleware bindingExtension:
|
||||
Use(bindingExtension);
|
||||
break;
|
||||
|
||||
case IMessageMiddleware messageExtension:
|
||||
configInstance.Use(messageExtension);
|
||||
break;
|
||||
|
||||
case IPublishMiddleware publishExtension:
|
||||
configInstance.Use(publishExtension);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"Unsupported middleware implementation: {(middleware == null ? "null" : middleware.GetType().Name)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var bindingBundle = (extension as ITapetiExtensionBinding)?.GetBindings(DependencyResolver);
|
||||
if (bindingBundle == null)
|
||||
return this;
|
||||
|
||||
foreach (var binding in bindingBundle)
|
||||
config.RegisterBinding(binding);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <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 can accept those consequences.
|
||||
/// </summary>
|
||||
public TapetiConfig DisablePublisherConfirms()
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterBinding(IBinding binding)
|
||||
{
|
||||
usePublisherConfirms = false;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/// <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.
|
||||
/// Registers the default implementation of various Tapeti interfaces into the IoC container.
|
||||
/// </summary>
|
||||
public TapetiConfig SetPublisherConfirms(bool enabled)
|
||||
protected void RegisterDefaults()
|
||||
{
|
||||
usePublisherConfirms = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterDefaults()
|
||||
{
|
||||
if (!(dependencyResolver is IDependencyContainer container))
|
||||
if (!(DependencyResolver is IDependencyContainer container))
|
||||
return;
|
||||
|
||||
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),
|
||||
lastHandler);
|
||||
}
|
||||
|
||||
if (!controller.IsInterface)
|
||||
(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))
|
||||
private Config GetConfig()
|
||||
{
|
||||
if (config == null)
|
||||
throw new InvalidOperationException("TapetiConfig can not be updated after Build");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
internal class Config : ITapetiConfig
|
||||
{
|
||||
private readonly ConfigFeatures features = new ConfigFeatures();
|
||||
private readonly ConfigMiddleware middleware = new ConfigMiddleware();
|
||||
private readonly ConfigBindings bindings = new ConfigBindings();
|
||||
|
||||
public IDependencyResolver DependencyResolver { get; }
|
||||
public ITapetiConfigFeatues Features => features;
|
||||
public ITapetiConfigMiddleware Middleware => middleware;
|
||||
public ITapetiConfigBindings Bindings => bindings;
|
||||
|
||||
|
||||
public Config(IDependencyResolver dependencyResolver)
|
||||
{
|
||||
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);
|
||||
DependencyResolver = dependencyResolver;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig RegisterAllControllers(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute))))
|
||||
RegisterController(type);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig RegisterAllControllers()
|
||||
{
|
||||
return RegisterAllControllers(Assembly.GetEntryAssembly());
|
||||
}
|
||||
|
||||
private void RegisterCustomBindings()
|
||||
{
|
||||
foreach (var customBinding in customBindings)
|
||||
public void Lock()
|
||||
{
|
||||
// TODO Do we need to configure additional middleware, or does this only get confused if there is no MessageClass
|
||||
bindings.Lock();
|
||||
}
|
||||
|
||||
var binding = new CustomBinding(customBinding);
|
||||
if (binding.QueueInfo.Dynamic == false)
|
||||
{
|
||||
AddStaticRegistration(binding);
|
||||
}
|
||||
else if (binding.MessageClass != null)
|
||||
{
|
||||
AddDynamicRegistration(binding);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddUniqueRegistration(binding);
|
||||
}
|
||||
|
||||
public void Use(IMessageMiddleware handler)
|
||||
{
|
||||
middleware.Use(handler);
|
||||
}
|
||||
|
||||
public void Use(IPublishMiddleware handler)
|
||||
{
|
||||
middleware.Use(handler);
|
||||
}
|
||||
|
||||
|
||||
public void RegisterBinding(IBinding binding)
|
||||
{
|
||||
bindings.Add(binding);
|
||||
}
|
||||
|
||||
|
||||
public void SetPublisherConfirms(bool enabled)
|
||||
{
|
||||
features.PublisherConfirms = enabled;
|
||||
}
|
||||
|
||||
public void SetDeclareDurableQueues(bool enabled)
|
||||
{
|
||||
features.DeclareDurableQueues = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class ConfigFeatures : ITapetiConfigFeatues
|
||||
{
|
||||
public bool PublisherConfirms { get; internal set; } = true;
|
||||
public bool DeclareDurableQueues { get; internal set; } = true;
|
||||
}
|
||||
|
||||
|
||||
internal class ConfigMiddleware : ITapetiConfigMiddleware
|
||||
{
|
||||
private readonly List<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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (String.IsNullOrEmpty(prefix))
|
||||
@ -457,300 +432,6 @@ namespace Tapeti
|
||||
|
||||
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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Connection;
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
// TODO more separation from the actual worker / RabbitMQ Client for unit testing purposes
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public delegate void DisconnectedEventHandler(object sender, DisconnectedEventArgs e);
|
||||
|
||||
public class TapetiConnection : IDisposable
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// 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; }
|
||||
|
||||
private readonly Lazy<TapetiWorker> worker;
|
||||
private readonly Lazy<TapetiClient> client;
|
||||
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;
|
||||
(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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler Connected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event DisconnectedEventHandler Disconnected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler Reconnected;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ISubscriber> Subscribe(bool startConsuming = true)
|
||||
{
|
||||
if (subscriber == null)
|
||||
{
|
||||
subscriber = new TapetiSubscriber(() => worker.Value, config.Queues.ToList());
|
||||
await subscriber.BindQueues();
|
||||
subscriber = new TapetiSubscriber(() => client.Value, config);
|
||||
await subscriber.ApplyBindings();
|
||||
}
|
||||
|
||||
if (startConsuming)
|
||||
@ -50,30 +72,35 @@ namespace Tapeti
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISubscriber SubscribeSync(bool startConsuming = true)
|
||||
{
|
||||
return Subscribe(startConsuming).Result;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPublisher GetPublisher()
|
||||
{
|
||||
return new TapetiPublisher(() => worker.Value);
|
||||
return new TapetiPublisher(config, () => client.Value);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Close()
|
||||
{
|
||||
if (worker.IsValueCreated)
|
||||
await worker.Value.Close();
|
||||
if (client.IsValueCreated)
|
||||
await client.Value.Close();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Close().Wait();
|
||||
}
|
||||
|
||||
|
||||
private class ConnectionEventListener: IConnectionEventListener
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var reconnectedEvent = Reconnected;
|
||||
if (reconnectedEvent == null)
|
||||
return;
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class TapetiConnectionParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The hostname to connect to. Defaults to localhost.
|
||||
/// </summary>
|
||||
public string HostName { get; set; } = "localhost";
|
||||
|
||||
/// <summary>
|
||||
/// The port to connect to. Defaults to 5672.
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 5672;
|
||||
|
||||
/// <summary>
|
||||
/// The virtual host in RabbitMQ. Defaults to /.
|
||||
/// </summary>
|
||||
public string VirtualHost { get; set; } = "/";
|
||||
|
||||
/// <summary>
|
||||
/// The username to authenticate with. Defaults to guest.
|
||||
/// </summary>
|
||||
public string Username { get; set; } = "guest";
|
||||
|
||||
/// <summary>
|
||||
/// The password to authenticate with. Defaults to guest.
|
||||
/// </summary>
|
||||
public string Password { get; set; } = "guest";
|
||||
|
||||
/// <summary>
|
||||
@ -20,10 +42,17 @@ namespace Tapeti
|
||||
public ushort PrefetchCount { get; set; } = 50;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
{
|
||||
HostName = uri.Host;
|
||||
|
@ -6,6 +6,10 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Tasks
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// An implementation of a queue which runs tasks on a single thread.
|
||||
/// </summary>
|
||||
public class SingleThreadTaskQueue : IDisposable
|
||||
{
|
||||
private readonly object previousTaskLock = new object();
|
||||
@ -14,6 +18,10 @@ namespace Tapeti.Tasks
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
lock (previousTaskLock)
|
||||
@ -45,89 +57,90 @@ namespace Tapeti.Tasks
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (singleThreadScheduler.IsValueCreated)
|
||||
singleThreadScheduler.Value.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SingleThreadTaskScheduler : TaskScheduler, IDisposable
|
||||
{
|
||||
public override int MaximumConcurrencyLevel => 1;
|
||||
|
||||
|
||||
private readonly Queue<Task> scheduledTasks = new Queue<Task>();
|
||||
private bool disposed;
|
||||
|
||||
|
||||
public SingleThreadTaskScheduler()
|
||||
internal class SingleThreadTaskScheduler : TaskScheduler, IDisposable
|
||||
{
|
||||
// ReSharper disable once ObjectCreationAsStatement - fire and forget!
|
||||
new Thread(WorkerThread).Start();
|
||||
}
|
||||
public override int MaximumConcurrencyLevel => 1;
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (scheduledTasks)
|
||||
private readonly Queue<Task> scheduledTasks = new Queue<Task>();
|
||||
private bool disposed;
|
||||
|
||||
|
||||
public SingleThreadTaskScheduler()
|
||||
{
|
||||
disposed = true;
|
||||
Monitor.PulseAll(scheduledTasks);
|
||||
// ReSharper disable once ObjectCreationAsStatement - fire and forget!
|
||||
new Thread(WorkerThread).Start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
if (disposed) return;
|
||||
|
||||
lock (scheduledTasks)
|
||||
public void Dispose()
|
||||
{
|
||||
scheduledTasks.Enqueue(task);
|
||||
Monitor.Pulse(scheduledTasks);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
{
|
||||
lock (scheduledTasks)
|
||||
{
|
||||
return scheduledTasks.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void WorkerThread()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
Task task;
|
||||
lock (scheduledTasks)
|
||||
{
|
||||
task = WaitAndDequeueTask();
|
||||
disposed = true;
|
||||
Monitor.PulseAll(scheduledTasks);
|
||||
}
|
||||
|
||||
if (task == null)
|
||||
break;
|
||||
|
||||
TryExecuteTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
private Task WaitAndDequeueTask()
|
||||
{
|
||||
while (!scheduledTasks.Any() && !disposed)
|
||||
Monitor.Wait(scheduledTasks);
|
||||
|
||||
return disposed ? null : scheduledTasks.Dequeue();
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
if (disposed) return;
|
||||
|
||||
lock (scheduledTasks)
|
||||
{
|
||||
scheduledTasks.Enqueue(task);
|
||||
Monitor.Pulse(scheduledTasks);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
{
|
||||
lock (scheduledTasks)
|
||||
{
|
||||
return scheduledTasks.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void WorkerThread()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Task task;
|
||||
lock (scheduledTasks)
|
||||
{
|
||||
task = WaitAndDequeueTask();
|
||||
}
|
||||
|
||||
if (task == null)
|
||||
break;
|
||||
|
||||
TryExecuteTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
private Task WaitAndDequeueTask()
|
||||
{
|
||||
while (!scheduledTasks.Any() && !disposed)
|
||||
Monitor.Wait(scheduledTasks);
|
||||
|
||||
return disposed ? null : scheduledTasks.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user