[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:
Mark van Renswoude 2019-08-13 20:30:04 +02:00
parent 70f394f7fe
commit f8fca5879c
85 changed files with 3069 additions and 1716 deletions

View File

@ -5,4 +5,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
</Project>

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View 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);
}
}
}

View File

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

View File

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

View File

@ -25,7 +25,6 @@ namespace Tapeti.Flow
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{
yield return new FlowBindingMiddleware();
yield return new FlowCleanupMiddleware();
}
}
}

View File

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

View File

@ -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" />

View File

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

View File

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

View File

@ -2,7 +2,7 @@
using Tapeti.Default;
using Xunit;
namespace Tapeti.Tests
namespace Tapeti.Tests.Default
{
// ReSharper disable InconsistentNaming
public class TypeNameRoutingKeyStrategyTests

View File

@ -1,9 +1,8 @@
using Tapeti.Helpers;
using Xunit;
namespace Tapeti.Tests
namespace Tapeti.Tests.Helpers
{
// ReSharper disable InconsistentNaming
public class ConnectionStringParserTest
{
[Fact]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -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">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</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
View 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);
}
}

View File

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

View File

@ -1,9 +0,0 @@
using System;
namespace Tapeti.Config
{
public interface IBindingMiddleware
{
void Handle(IBindingContext context, Action next);
}
}

View File

@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace Tapeti.Config
{
public interface ICleanupMiddleware
{
Task Handle(IMessageContext context, HandlingResult handlingResult);
}
}

View File

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

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

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

View 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);
}
}

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

View 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
{
}
}

View File

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

View File

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

View File

@ -1,10 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Tapeti.Config
{
public interface IMessageFilterMiddleware
{
Task Handle(IMessageContext context, Func<Task> next);
}
}

View File

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

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

View File

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

View 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);
}
*/
}

View 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);
}
}

View File

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

View 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);
}
}

View File

@ -1,10 +0,0 @@
using System.Collections.Generic;
namespace Tapeti.Config
{
public interface ITapetiExtentionBinding
{
IEnumerable<ICustomBinding> GetBindings(IDependencyResolver dependencyResolver);
}
}

View File

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

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

View 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);
}
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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);
}
}
}

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

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

View File

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

View 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));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />

View File

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

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

View File

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

View File

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

View File

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