Merge branch 'feature/multiqueuecontrollers' into develop
This commit is contained in:
commit
0c4f8a04f0
13
Annotations/DynamicQueueAttribute.cs
Normal file
13
Annotations/DynamicQueueAttribute.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Annotations
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a non-durable auto-delete queue to receive messages. Can be used
|
||||
/// on an entire MessageController class or on individual methods.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class DynamicQueueAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
9
Annotations/MessageControllerAttribute.cs
Normal file
9
Annotations/MessageControllerAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Annotations
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class MessageControllerAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Annotations
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class QueueAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool Dynamic { get; set; }
|
||||
|
||||
|
||||
public QueueAttribute(string name = null)
|
||||
{
|
||||
Name = name;
|
||||
Dynamic = (name == null);
|
||||
}
|
||||
}
|
||||
}
|
25
Annotations/StaticQueueAttribute.cs
Normal file
25
Annotations/StaticQueueAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Annotations
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds to an existing durable queue to receive messages. Can be used
|
||||
/// on an entire MessageController class or on individual methods.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// At the moment there is no support for creating a durable queue and managing the
|
||||
/// bindings. The author recommends https://git.x2software.net/pub/RabbitMetaQueue
|
||||
/// for deploy-time management of durable queues (shameless plug intended).
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class StaticQueueAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
|
||||
public StaticQueueAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
24
Config/IBindingContext.cs
Normal file
24
Config/IBindingContext.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public delegate object ValueFactory(IMessageContext context);
|
||||
|
||||
|
||||
public interface IBindingContext
|
||||
{
|
||||
Type MessageClass { get; set; }
|
||||
IReadOnlyList<IBindingParameter> Parameters { get; }
|
||||
}
|
||||
|
||||
|
||||
public interface IBindingParameter
|
||||
{
|
||||
ParameterInfo Info { get; }
|
||||
bool HasBinding { get; }
|
||||
|
||||
void SetBinding(ValueFactory valueFactory);
|
||||
}
|
||||
}
|
9
Config/IBindingMiddleware.cs
Normal file
9
Config/IBindingMiddleware.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public interface IBindingMiddleware
|
||||
{
|
||||
void Handle(IBindingContext context, Action next);
|
||||
}
|
||||
}
|
35
Config/IConfig.cs
Normal file
35
Config/IConfig.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public interface IConfig
|
||||
{
|
||||
string Exchange { get; }
|
||||
IDependencyResolver DependencyResolver { get; }
|
||||
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||
IEnumerable<IQueue> Queues { get; }
|
||||
}
|
||||
|
||||
|
||||
public interface IQueue
|
||||
{
|
||||
bool Dynamic { get; }
|
||||
string Name { get; }
|
||||
|
||||
IEnumerable<IBinding> Bindings { get; }
|
||||
}
|
||||
|
||||
|
||||
public interface IBinding
|
||||
{
|
||||
Type Controller { get; }
|
||||
MethodInfo Method { get; }
|
||||
Type MessageClass { get; }
|
||||
|
||||
bool Accept(object message);
|
||||
Task<object> Invoke(IMessageContext context, object message);
|
||||
}
|
||||
}
|
11
Config/IMessageContext.cs
Normal file
11
Config/IMessageContext.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public interface IMessageContext
|
||||
{
|
||||
object Controller { get; }
|
||||
object Message { get; }
|
||||
IDictionary<string, object> Items { get; }
|
||||
}
|
||||
}
|
9
Config/IMessageMiddleware.cs
Normal file
9
Config/IMessageMiddleware.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public interface IMessageMiddleware
|
||||
{
|
||||
void Handle(IMessageContext context, Action next);
|
||||
}
|
||||
}
|
9
Config/IMiddlewareBundle.cs
Normal file
9
Config/IMiddlewareBundle.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti.Config
|
||||
{
|
||||
public interface IMiddlewareBundle
|
||||
{
|
||||
IEnumerable<object> GetContents(IDependencyResolver dependencyResolver);
|
||||
}
|
||||
}
|
@ -1,21 +1,26 @@
|
||||
using System;
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Helpers;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
{
|
||||
public class TapetiConsumer : DefaultBasicConsumer
|
||||
{
|
||||
private readonly TapetiWorker worker;
|
||||
private readonly IMessageSerializer messageSerializer;
|
||||
private readonly IQueueRegistration queueRegistration;
|
||||
private readonly IDependencyResolver dependencyResolver;
|
||||
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
||||
private readonly List<IBinding> bindings;
|
||||
|
||||
|
||||
public TapetiConsumer(TapetiWorker worker, IMessageSerializer messageSerializer, IQueueRegistration queueRegistration)
|
||||
public TapetiConsumer(TapetiWorker worker, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware)
|
||||
{
|
||||
this.worker = worker;
|
||||
this.messageSerializer = messageSerializer;
|
||||
this.queueRegistration = queueRegistration;
|
||||
this.dependencyResolver = dependencyResolver;
|
||||
this.messageMiddleware = messageMiddleware;
|
||||
this.bindings = bindings.ToList();
|
||||
}
|
||||
|
||||
|
||||
@ -24,22 +29,46 @@ namespace Tapeti.Connection
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = messageSerializer.Deserialize(body, properties);
|
||||
var message = dependencyResolver.Resolve<IMessageSerializer>().Deserialize(body, properties);
|
||||
if (message == null)
|
||||
throw new ArgumentException("Empty message");
|
||||
|
||||
if (queueRegistration.Accept(message))
|
||||
queueRegistration.Visit(message);
|
||||
else
|
||||
var handled = false;
|
||||
foreach (var binding in bindings.Where(b => b.Accept(message)))
|
||||
{
|
||||
var context = new MessageContext
|
||||
{
|
||||
Controller = dependencyResolver.Resolve(binding.Controller),
|
||||
Message = message
|
||||
};
|
||||
|
||||
MiddlewareHelper.Go(messageMiddleware, (handler, next) => handler.Handle(context, next));
|
||||
|
||||
var result = binding.Invoke(context, message).Result;
|
||||
if (result != null)
|
||||
worker.Publish(result);
|
||||
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (!handled)
|
||||
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
|
||||
|
||||
worker.Respond(deliveryTag, ConsumeResponse.Ack);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//TODO pluggable exception handling
|
||||
worker.Respond(deliveryTag, ConsumeResponse.Nack);
|
||||
worker.Respond(deliveryTag, ConsumeResponse.Requeue);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class MessageContext : IMessageContext
|
||||
{
|
||||
public object Controller { get; set; }
|
||||
public object Message { get; set; }
|
||||
public IDictionary<string, object> Items { get; } = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,22 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
{
|
||||
public class TapetiPublisher : IPublisher
|
||||
{
|
||||
private readonly TapetiWorker worker;
|
||||
private readonly Func<TapetiWorker> workerFactory;
|
||||
|
||||
|
||||
public TapetiPublisher(TapetiWorker worker)
|
||||
public TapetiPublisher(Func<TapetiWorker> workerFactory)
|
||||
{
|
||||
this.worker = worker;
|
||||
this.workerFactory = workerFactory;
|
||||
}
|
||||
|
||||
|
||||
public Task Publish(object message)
|
||||
{
|
||||
return worker.Publish(message);
|
||||
return workerFactory().Publish(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
{
|
||||
public class TapetiSubscriber : ISubscriber
|
||||
{
|
||||
private readonly TapetiWorker worker;
|
||||
private readonly Func<TapetiWorker> workerFactory;
|
||||
|
||||
|
||||
public TapetiSubscriber(TapetiWorker worker)
|
||||
public TapetiSubscriber(Func<TapetiWorker> workerFactory)
|
||||
{
|
||||
this.worker = worker;
|
||||
this.workerFactory = workerFactory;
|
||||
}
|
||||
|
||||
|
||||
public async Task BindQueues(IEnumerable<IQueueRegistration> registrations)
|
||||
public async Task BindQueues(IEnumerable<IQueue> queues)
|
||||
{
|
||||
await Task.WhenAll(registrations.Select(registration => worker.Subscribe(registration)).ToList());
|
||||
await Task.WhenAll(queues.Select(queue => workerFactory().Subscribe(queue)).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Exceptions;
|
||||
using RabbitMQ.Client.Framing;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Tasks;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
@ -10,20 +12,23 @@ namespace Tapeti.Connection
|
||||
public class TapetiWorker
|
||||
{
|
||||
public TapetiConnectionParams ConnectionParams { get; set; }
|
||||
public string PublishExchange { get; set; }
|
||||
|
||||
public string Exchange { get; set; }
|
||||
|
||||
private readonly IDependencyResolver dependencyResolver;
|
||||
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
||||
private readonly IMessageSerializer messageSerializer;
|
||||
private readonly IRoutingKeyStrategy routingKeyStrategy;
|
||||
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
|
||||
private IConnection connection;
|
||||
private IModel channel;
|
||||
private RabbitMQ.Client.IConnection connection;
|
||||
private IModel channelInstance;
|
||||
|
||||
|
||||
public TapetiWorker(IMessageSerializer messageSerializer, IRoutingKeyStrategy routingKeyStrategy)
|
||||
public TapetiWorker(IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware)
|
||||
{
|
||||
this.messageSerializer = messageSerializer;
|
||||
this.routingKeyStrategy = routingKeyStrategy;
|
||||
this.dependencyResolver = dependencyResolver;
|
||||
this.messageMiddleware = messageMiddleware;
|
||||
messageSerializer = dependencyResolver.Resolve<IMessageSerializer>();
|
||||
routingKeyStrategy = dependencyResolver.Resolve<IRoutingKeyStrategy>();
|
||||
}
|
||||
|
||||
|
||||
@ -35,29 +40,45 @@ namespace Tapeti.Connection
|
||||
var body = messageSerializer.Serialize(message, properties);
|
||||
|
||||
(await GetChannel())
|
||||
.BasicPublish(PublishExchange, routingKeyStrategy.GetRoutingKey(message.GetType()), false,
|
||||
.BasicPublish(Exchange, routingKeyStrategy.GetRoutingKey(message.GetType()), false,
|
||||
properties, body);
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
|
||||
public Task Subscribe(string queueName, IQueueRegistration queueRegistration)
|
||||
public Task Consume(string queueName, IEnumerable<IBinding> bindings)
|
||||
{
|
||||
return taskQueue.Value.Add(async () =>
|
||||
{
|
||||
(await GetChannel())
|
||||
.BasicConsume(queueName, false, new TapetiConsumer(this, messageSerializer, queueRegistration));
|
||||
(await GetChannel()).BasicConsume(queueName, false, new TapetiConsumer(this, dependencyResolver, bindings, messageMiddleware));
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
|
||||
public async Task Subscribe(IQueueRegistration registration)
|
||||
public async Task Subscribe(IQueue queue)
|
||||
{
|
||||
var queueName = await taskQueue.Value.Add(async () =>
|
||||
registration.BindQueue(await GetChannel()))
|
||||
.Unwrap();
|
||||
var queueName = await taskQueue.Value.Add(async () =>
|
||||
{
|
||||
var channel = await GetChannel();
|
||||
|
||||
await Subscribe(queueName, registration);
|
||||
if (queue.Dynamic)
|
||||
{
|
||||
var dynamicQueue = channel.QueueDeclare();
|
||||
|
||||
foreach (var binding in queue.Bindings)
|
||||
{
|
||||
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
|
||||
channel.QueueBind(dynamicQueue.QueueName, Exchange, routingKey);
|
||||
}
|
||||
|
||||
return dynamicQueue.QueueName;
|
||||
}
|
||||
|
||||
channel.QueueDeclarePassive(queue.Name);
|
||||
return queue.Name;
|
||||
}).Unwrap();
|
||||
|
||||
await Consume(queueName, queue.Bindings);
|
||||
}
|
||||
|
||||
|
||||
@ -91,10 +112,10 @@ namespace Tapeti.Connection
|
||||
|
||||
return taskQueue.Value.Add(() =>
|
||||
{
|
||||
if (channel != null)
|
||||
if (channelInstance != null)
|
||||
{
|
||||
channel.Dispose();
|
||||
channel = null;
|
||||
channelInstance.Dispose();
|
||||
channelInstance = null;
|
||||
}
|
||||
|
||||
// ReSharper disable once InvertIf
|
||||
@ -115,8 +136,8 @@ namespace Tapeti.Connection
|
||||
/// </remarks>
|
||||
private async Task<IModel> GetChannel()
|
||||
{
|
||||
if (channel != null)
|
||||
return channel;
|
||||
if (channelInstance != null)
|
||||
return channelInstance;
|
||||
|
||||
var connectionFactory = new ConnectionFactory
|
||||
{
|
||||
@ -134,7 +155,7 @@ namespace Tapeti.Connection
|
||||
try
|
||||
{
|
||||
connection = connectionFactory.CreateConnection();
|
||||
channel = connection.CreateModel();
|
||||
channelInstance = connection.CreateModel();
|
||||
|
||||
break;
|
||||
}
|
||||
@ -144,7 +165,7 @@ namespace Tapeti.Connection
|
||||
}
|
||||
}
|
||||
|
||||
return channel;
|
||||
return channelInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
Default/BindingBufferStop.cs
Normal file
13
Default/BindingBufferStop.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
// End of the line...
|
||||
public class BindingBufferStop : IBindingMiddleware
|
||||
{
|
||||
public void Handle(IBindingContext context, Action next)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
public class DefaultControllerFactory : IControllerFactory
|
||||
{
|
||||
private readonly Dictionary<Type, Func<object>> controllerConstructors = new Dictionary<Type, Func<object>>();
|
||||
private readonly Func<IPublisher> publisherFactory;
|
||||
|
||||
public DefaultControllerFactory(Func<IPublisher> publisherFactory)
|
||||
{
|
||||
this.publisherFactory = publisherFactory;
|
||||
}
|
||||
|
||||
|
||||
public object CreateController(Type controllerType)
|
||||
{
|
||||
Func<object> constructor;
|
||||
if (!controllerConstructors.TryGetValue(controllerType, out constructor))
|
||||
throw new ArgumentException($"Can not create unregistered controller {controllerType.FullName}");
|
||||
|
||||
return constructor();
|
||||
}
|
||||
|
||||
|
||||
public void RegisterController(Type type)
|
||||
{
|
||||
controllerConstructors.Add(type, GetConstructor(type));
|
||||
}
|
||||
|
||||
|
||||
protected Func<object> GetConstructor(Type type)
|
||||
{
|
||||
var constructors = type.GetConstructors();
|
||||
|
||||
ConstructorInfo publisherConstructor = null;
|
||||
ConstructorInfo emptyConstructor = null;
|
||||
|
||||
foreach (var constructor in constructors)
|
||||
{
|
||||
var parameters = constructor.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IPublisher))
|
||||
publisherConstructor = constructor;
|
||||
}
|
||||
else
|
||||
emptyConstructor = constructor;
|
||||
}
|
||||
|
||||
if (publisherConstructor != null)
|
||||
return () => publisherConstructor.Invoke(new object[] { publisherFactory() });
|
||||
|
||||
if (emptyConstructor != null)
|
||||
return () => emptyConstructor.Invoke(null);
|
||||
|
||||
throw new ArgumentException($"Unable to construct type {type.Name}, a parameterless constructor or one with only an IPublisher parameter is required");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
/**
|
||||
* !! IoC Container 9000 !!
|
||||
*
|
||||
* ...you probably want to replace this one as soon as possible.
|
||||
*
|
||||
* A Simple Injector implementation is provided in the Tapeti.SimpleInjector package.
|
||||
*/
|
||||
public class DefaultDependencyResolver : IDependencyInjector
|
||||
{
|
||||
private readonly Lazy<DefaultControllerFactory> controllerFactory;
|
||||
private readonly Lazy<DefaultRoutingKeyStrategy> routingKeyStrategy = new Lazy<DefaultRoutingKeyStrategy>();
|
||||
private readonly Lazy<DefaultMessageSerializer> messageSerializer = new Lazy<DefaultMessageSerializer>();
|
||||
private readonly Lazy<ILogger> logger;
|
||||
|
||||
|
||||
|
||||
public DefaultDependencyResolver(Func<IPublisher> publisherFactory)
|
||||
{
|
||||
controllerFactory = new Lazy<DefaultControllerFactory>(() => new DefaultControllerFactory(publisherFactory));
|
||||
|
||||
logger = new Lazy<ILogger>(() =>
|
||||
{
|
||||
// http://stackoverflow.com/questions/6408588/how-to-tell-if-there-is-a-console
|
||||
try
|
||||
{
|
||||
// ReSharper disable once UnusedVariable
|
||||
var dummy = Console.WindowHeight;
|
||||
|
||||
return new ConsoleLogger();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DevNullLogger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public T Resolve<T>() where T : class
|
||||
{
|
||||
if (typeof(T) == typeof(IControllerFactory))
|
||||
return (T)(controllerFactory.Value as IControllerFactory);
|
||||
|
||||
if (typeof(T) == typeof(IRoutingKeyStrategy))
|
||||
return (T)(routingKeyStrategy.Value as IRoutingKeyStrategy);
|
||||
|
||||
if (typeof(T) == typeof(IMessageSerializer))
|
||||
return (T)(messageSerializer.Value as IMessageSerializer);
|
||||
|
||||
if (typeof(T) == typeof(ILogger))
|
||||
return (T)logger.Value;
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
|
||||
public void RegisterController(Type type)
|
||||
{
|
||||
controllerFactory.Value.RegisterController(type);
|
||||
}
|
||||
}
|
||||
}
|
26
Default/DependencyResolverBinding.cs
Normal file
26
Default/DependencyResolverBinding.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
public class DependencyResolverBinding : IBindingMiddleware
|
||||
{
|
||||
private readonly IDependencyResolver resolver;
|
||||
|
||||
|
||||
public DependencyResolverBinding(IDependencyResolver resolver)
|
||||
{
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
|
||||
public void Handle(IBindingContext context, Action next)
|
||||
{
|
||||
next();
|
||||
|
||||
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType.IsClass))
|
||||
parameter.SetBinding(messageContext => resolver.Resolve(parameter.Info.ParameterType));
|
||||
}
|
||||
}
|
||||
}
|
23
Default/MessageBinding.cs
Normal file
23
Default/MessageBinding.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Default
|
||||
{
|
||||
public class MessageBinding : IBindingMiddleware
|
||||
{
|
||||
public void Handle(IBindingContext context, Action next)
|
||||
{
|
||||
if (context.Parameters.Count == 0)
|
||||
throw new TopologyConfigurationException("First parameter must be a message class");
|
||||
|
||||
var parameter = context.Parameters[0];
|
||||
if (!parameter.Info.ParameterType.IsClass)
|
||||
throw new TopologyConfigurationException($"First parameter {parameter.Info.Name} must be a message class");
|
||||
|
||||
parameter.SetBinding(messageContext => messageContext.Message);
|
||||
context.MessageClass = parameter.Info.ParameterType;
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
22
Helpers/ConsoleHelper.cs
Normal file
22
Helpers/ConsoleHelper.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Helpers
|
||||
{
|
||||
public static class ConsoleHelper
|
||||
{
|
||||
// Source: http://stackoverflow.com/questions/6408588/how-to-tell-if-there-is-a-console
|
||||
public static bool IsAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
// ReSharper disable once UnusedVariable - that's why it's called dummy
|
||||
var dummy = Console.WindowHeight;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
Helpers/MiddlewareHelper.cs
Normal file
26
Helpers/MiddlewareHelper.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti.Helpers
|
||||
{
|
||||
public static class MiddlewareHelper
|
||||
{
|
||||
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle)
|
||||
{
|
||||
var handlerIndex = middleware.Count - 1;
|
||||
if (handlerIndex == -1)
|
||||
return;
|
||||
|
||||
Action handleNext = null;
|
||||
|
||||
handleNext = () =>
|
||||
{
|
||||
handlerIndex--;
|
||||
if (handlerIndex >= 0)
|
||||
handle(middleware[handlerIndex], handleNext);
|
||||
};
|
||||
|
||||
handle(middleware[handlerIndex], handleNext);
|
||||
}
|
||||
}
|
||||
}
|
10
IConnection.cs
Normal file
10
IConnection.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface IConnection : IDisposable
|
||||
{
|
||||
Task<ISubscriber> Subscribe();
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface IControllerFactory
|
||||
{
|
||||
object CreateController(Type controllerType);
|
||||
}
|
||||
}
|
@ -5,11 +5,14 @@ namespace Tapeti
|
||||
public interface IDependencyResolver
|
||||
{
|
||||
T Resolve<T>() where T : class;
|
||||
object Resolve(Type type);
|
||||
}
|
||||
|
||||
|
||||
public interface IDependencyInjector : IDependencyResolver
|
||||
{
|
||||
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||
void RegisterPublisher(Func<IPublisher> publisher);
|
||||
void RegisterController(Type type);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface IQueueRegistration
|
||||
{
|
||||
string BindQueue(IModel channel);
|
||||
|
||||
bool Accept(object message);
|
||||
Task Visit(object message);
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface ISubscriber
|
||||
{
|
||||
|
16
MessageController.cs
Normal file
16
MessageController.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Tapeti.Annotations;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for message controllers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Using this base class is not required, you can add the MessageController attribute
|
||||
/// to any class.
|
||||
/// </remarks>
|
||||
[MessageController]
|
||||
public abstract class MessageController
|
||||
{
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
|
15
README.md
15
README.md
@ -1,3 +1,14 @@
|
||||
'Small to medium-sized and classified as "Least Concern" by the IUCN.'
|
||||
# Tapeti
|
||||
> 'Small to medium-sized and classified as "Least Concern" by the IUCN.'
|
||||
>
|
||||
> [_Wikipedia_](https://en.wikipedia.org/wiki/Tapeti)
|
||||
|
||||
- Wikipedia
|
||||
Tapeti is a wrapper for the RabbitMQ .NET client designed for long-running microservices with a few specific goals:
|
||||
|
||||
1. Automatic registration of message handlers
|
||||
2. Publishing without transport details
|
||||
* Routing key generated based on class name
|
||||
* One exchange (per service / group of services) to publish them all
|
||||
3. Attribute based, no base class requirements (only for convenience)
|
||||
4. Graceful handling of connection issues, even at startup
|
||||
5. Basic Saga support
|
@ -1,142 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using RabbitMQ.Client;
|
||||
using Tapeti.Annotations;
|
||||
|
||||
namespace Tapeti.Registration
|
||||
{
|
||||
using MessageHandlerAction = Func<object, Task>;
|
||||
|
||||
public struct MessageHandler
|
||||
{
|
||||
public MessageHandlerAction Action;
|
||||
public string Exchange;
|
||||
public string RoutingKey;
|
||||
}
|
||||
|
||||
|
||||
public abstract class AbstractControllerRegistration : IQueueRegistration
|
||||
{
|
||||
private readonly Func<IControllerFactory> controllerFactoryFactory;
|
||||
private readonly Type controllerType;
|
||||
private readonly string defaultExchange;
|
||||
private readonly Dictionary<Type, List<MessageHandler>> messageHandlers = new Dictionary<Type, List<MessageHandler>>();
|
||||
|
||||
|
||||
protected AbstractControllerRegistration(Func<IControllerFactory> controllerFactoryFactory, Type controllerType, string defaultExchange)
|
||||
{
|
||||
this.controllerFactoryFactory = controllerFactoryFactory;
|
||||
this.controllerType = controllerType;
|
||||
this.defaultExchange = defaultExchange;
|
||||
|
||||
// ReSharper disable once VirtualMemberCallInConstructor - I know. What do you think this is, C++?
|
||||
GetMessageHandlers(controllerType, (type, handler) =>
|
||||
{
|
||||
if (!messageHandlers.ContainsKey(type))
|
||||
messageHandlers.Add(type, new List<MessageHandler> { handler });
|
||||
else
|
||||
messageHandlers[type].Add(handler);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected virtual void GetMessageHandlers(Type type, Action<Type, MessageHandler> add)
|
||||
{
|
||||
foreach (var method in type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object))
|
||||
.Select(m => (MethodInfo)m))
|
||||
{
|
||||
Type messageType;
|
||||
var messageHandler = GetMessageHandler(method, out messageType);
|
||||
|
||||
add(messageType, messageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected virtual MessageHandler GetMessageHandler(MethodInfo method, out Type messageType)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
if (parameters.Length != 1 || !parameters[0].ParameterType.IsClass)
|
||||
throw new ArgumentException($"Method {method.Name} does not have a single object parameter");
|
||||
|
||||
messageType = parameters[0].ParameterType;
|
||||
var messageHandler = new MessageHandler();
|
||||
|
||||
if (method.ReturnType == typeof(void))
|
||||
messageHandler.Action = CreateSyncMessageHandler(method);
|
||||
else if (method.ReturnType == typeof(Task))
|
||||
messageHandler.Action = CreateAsyncMessageHandler(method);
|
||||
else
|
||||
throw new ArgumentException($"Method {method.Name} needs to return void or a Task");
|
||||
|
||||
var exchangeAttribute = method.GetCustomAttribute<ExchangeAttribute>() ?? method.DeclaringType.GetCustomAttribute<ExchangeAttribute>();
|
||||
messageHandler.Exchange = exchangeAttribute?.Name;
|
||||
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
|
||||
protected IEnumerable<Type> GetMessageTypes()
|
||||
{
|
||||
return messageHandlers.Keys;
|
||||
}
|
||||
|
||||
|
||||
protected IEnumerable<string> GetMessageExchanges(Type type)
|
||||
{
|
||||
var exchanges = messageHandlers[type]
|
||||
.Where(h => h.Exchange != null)
|
||||
.Select(h => h.Exchange)
|
||||
.Distinct(StringComparer.InvariantCulture)
|
||||
.ToArray();
|
||||
|
||||
return exchanges.Length > 0 ? exchanges : new[] { defaultExchange };
|
||||
}
|
||||
|
||||
|
||||
public abstract string BindQueue(IModel channel);
|
||||
|
||||
|
||||
public bool Accept(object message)
|
||||
{
|
||||
return messageHandlers.ContainsKey(message.GetType());
|
||||
}
|
||||
|
||||
|
||||
public Task Visit(object message)
|
||||
{
|
||||
var registeredHandlers = messageHandlers[message.GetType()];
|
||||
if (registeredHandlers != null)
|
||||
return Task.WhenAll(registeredHandlers.Select(messageHandler => messageHandler.Action(message)));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
protected virtual MessageHandlerAction CreateSyncMessageHandler(MethodInfo method)
|
||||
{
|
||||
return message =>
|
||||
{
|
||||
var controller = controllerFactoryFactory().CreateController(controllerType);
|
||||
method.Invoke(controller, new[] { message });
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected virtual MessageHandlerAction CreateAsyncMessageHandler(MethodInfo method)
|
||||
{
|
||||
return message =>
|
||||
{
|
||||
var controller = controllerFactoryFactory().CreateController(controllerType);
|
||||
return (Task)method.Invoke(controller, new[] { message });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace Tapeti.Registration
|
||||
{
|
||||
public class ControllerDynamicQueueRegistration : AbstractControllerRegistration
|
||||
{
|
||||
private readonly Func<IRoutingKeyStrategy> routingKeyStrategyFactory;
|
||||
|
||||
|
||||
public ControllerDynamicQueueRegistration(Func<IControllerFactory> controllerFactoryFactory, Func<IRoutingKeyStrategy> routingKeyStrategyFactory, Type controllerType, string defaultExchange)
|
||||
: base(controllerFactoryFactory, controllerType, defaultExchange)
|
||||
{
|
||||
this.routingKeyStrategyFactory = routingKeyStrategyFactory;
|
||||
}
|
||||
|
||||
|
||||
public override string BindQueue(IModel channel)
|
||||
{
|
||||
var queue = channel.QueueDeclare();
|
||||
|
||||
foreach (var messageType in GetMessageTypes())
|
||||
{
|
||||
var routingKey = routingKeyStrategyFactory().GetRoutingKey(messageType);
|
||||
|
||||
foreach (var exchange in GetMessageExchanges(messageType))
|
||||
channel.QueueBind(queue.QueueName, exchange, routingKey);
|
||||
}
|
||||
|
||||
return queue.QueueName;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace Tapeti.Registration
|
||||
{
|
||||
public class ControllerQueueRegistration : AbstractControllerRegistration
|
||||
{
|
||||
private readonly string queueName;
|
||||
|
||||
public ControllerQueueRegistration(Func<IControllerFactory> controllerFactoryFactory, Type controllerType, string defaultExchange, string queueName) : base(controllerFactoryFactory, controllerType, defaultExchange)
|
||||
{
|
||||
this.queueName = queueName;
|
||||
}
|
||||
|
||||
|
||||
public override string BindQueue(IModel channel)
|
||||
{
|
||||
return channel.QueueDeclarePassive(queueName).QueueName;
|
||||
}
|
||||
}
|
||||
}
|
13
Tapeti.Saga/ISaga.cs
Normal file
13
Tapeti.Saga/ISaga.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public interface ISaga<out T> : IDisposable where T : class
|
||||
{
|
||||
string Id { get; }
|
||||
T State { get; }
|
||||
|
||||
void ExpectResponse(string callId);
|
||||
void ResolveResponse(string callId);
|
||||
}
|
||||
}
|
11
Tapeti.Saga/ISagaProvider.cs
Normal file
11
Tapeti.Saga/ISagaProvider.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public interface ISagaProvider
|
||||
{
|
||||
Task<ISaga<T>> Begin<T>() where T : class;
|
||||
Task<ISaga<T>> Continue<T>(string sagaId) where T : class;
|
||||
Task<ISaga<T>> Current<T>() where T : class;
|
||||
}
|
||||
}
|
10
Tapeti.Saga/ISagaStore.cs
Normal file
10
Tapeti.Saga/ISagaStore.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public interface ISagaStore
|
||||
{
|
||||
Task<object> Read(string sagaId);
|
||||
Task Update(string sagaId, object state);
|
||||
}
|
||||
}
|
36
Tapeti.Saga/Properties/AssemblyInfo.cs
Normal file
36
Tapeti.Saga/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Tapeti.Saga")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
||||
[assembly: AssemblyProduct("Tapeti.Saga")]
|
||||
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard Company 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("f84ad920-d5a1-455d-aed5-2542b3a47b85")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
28
Tapeti.Saga/SagaBindingMiddleware.cs
Normal file
28
Tapeti.Saga/SagaBindingMiddleware.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public class SagaBindingMiddleware : IBindingMiddleware
|
||||
{
|
||||
public void Handle(IBindingContext context, Action next)
|
||||
{
|
||||
foreach (var parameter in context.Parameters.Where(p =>
|
||||
p.Info.ParameterType.IsGenericType &&
|
||||
p.Info.ParameterType.GetGenericTypeDefinition() == typeof(ISaga<>)))
|
||||
{
|
||||
parameter.SetBinding(messageContext =>
|
||||
{
|
||||
object saga;
|
||||
if (!messageContext.Items.TryGetValue("Saga", out saga))
|
||||
return null;
|
||||
|
||||
return saga.GetType() == typeof(ISaga<>) ? saga : null;
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
43
Tapeti.Saga/SagaMemoryStore.cs
Normal file
43
Tapeti.Saga/SagaMemoryStore.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public class SagaMemoryStore : ISagaStore
|
||||
{
|
||||
private ISagaStore decoratedStore;
|
||||
private readonly Dictionary<string, object> values = new Dictionary<string, object>();
|
||||
|
||||
|
||||
// Not a constructor to allow standard injection to work when using only the MemoryStore
|
||||
public static SagaMemoryStore AsCacheFor(ISagaStore store)
|
||||
{
|
||||
return new SagaMemoryStore
|
||||
{
|
||||
decoratedStore = store
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public async Task<object> Read(string sagaId)
|
||||
{
|
||||
object value;
|
||||
|
||||
// ReSharper disable once InvertIf
|
||||
if (!values.TryGetValue(sagaId, out value) && decoratedStore != null)
|
||||
{
|
||||
value = await decoratedStore.Read(sagaId);
|
||||
values.Add(sagaId, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public async Task Update(string sagaId, object state)
|
||||
{
|
||||
values[sagaId] = state;
|
||||
if (decoratedStore != null)
|
||||
await decoratedStore.Update(sagaId, state);
|
||||
}
|
||||
}
|
||||
}
|
22
Tapeti.Saga/SagaMessageMiddleware.cs
Normal file
22
Tapeti.Saga/SagaMessageMiddleware.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public class SagaMessageMiddleware : IMessageMiddleware
|
||||
{
|
||||
private readonly IDependencyResolver dependencyResolver;
|
||||
|
||||
|
||||
public SagaMessageMiddleware(IDependencyResolver dependencyResolver)
|
||||
{
|
||||
this.dependencyResolver = dependencyResolver;
|
||||
}
|
||||
|
||||
public void Handle(IMessageContext context, Action next)
|
||||
{
|
||||
context.Items["Saga"] = dependencyResolver.Resolve<ISagaProvider>().Continue("");
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
16
Tapeti.Saga/SagaMiddleware.cs
Normal file
16
Tapeti.Saga/SagaMiddleware.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public class SagaMiddleware : IMiddlewareBundle
|
||||
{
|
||||
public IEnumerable<object> GetContents(IDependencyResolver dependencyResolver)
|
||||
{
|
||||
(dependencyResolver as IDependencyInjector)?.RegisterDefault<ISagaProvider, SagaProvider>();
|
||||
|
||||
yield return new SagaBindingMiddleware();
|
||||
yield return new SagaMessageMiddleware(dependencyResolver);
|
||||
}
|
||||
}
|
||||
}
|
90
Tapeti.Saga/SagaProvider.cs
Normal file
90
Tapeti.Saga/SagaProvider.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tapeti.Saga
|
||||
{
|
||||
public class SagaProvider : ISagaProvider
|
||||
{
|
||||
protected static readonly ConcurrentDictionary<string, SemaphoreSlim> SagaLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
private readonly ISagaStore store;
|
||||
|
||||
public SagaProvider(ISagaStore store)
|
||||
{
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ISaga<T>> Begin<T>(T initialState) where T : class
|
||||
{
|
||||
var saga = await Saga<T>.Create(() => Task.FromResult(initialState));
|
||||
await store.Update(saga.Id, saga.State);
|
||||
|
||||
return saga;
|
||||
}
|
||||
|
||||
public async Task<ISaga<T>> Continue<T>(string sagaId) where T : class
|
||||
{
|
||||
return await Saga<T>.Create(async () => await store.Read(sagaId) as T, sagaId);
|
||||
}
|
||||
|
||||
public async Task<object> Continue(string sagaId)
|
||||
{
|
||||
return new Saga<object>
|
||||
{
|
||||
Id = sagaId,
|
||||
State = await store.Read(sagaId)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected class Saga<T> : ISaga<T> where T : class
|
||||
{
|
||||
private bool disposed;
|
||||
|
||||
public string Id { get; set; }
|
||||
public T State { get; set; }
|
||||
|
||||
|
||||
public static async Task<Saga<T>> Create(Func<Task<T>> getState, string id = null)
|
||||
{
|
||||
var sagaId = id ?? Guid.NewGuid().ToString();
|
||||
await SagaLocks.GetOrAdd(sagaId, new SemaphoreSlim(1)).WaitAsync();
|
||||
|
||||
var saga = new Saga<T>
|
||||
{
|
||||
Id = sagaId,
|
||||
State = await getState()
|
||||
};
|
||||
|
||||
return saga;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
SemaphoreSlim semaphore;
|
||||
if (SagaLocks.TryGetValue(Id, out semaphore))
|
||||
semaphore.Release();
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
|
||||
public void ExpectResponse(string callId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public void ResolveResponse(string callId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
Tapeti.Saga/Tapeti.Saga.csproj
Normal file
56
Tapeti.Saga/Tapeti.Saga.csproj
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{F84AD920-D5A1-455D-AED5-2542B3A47B85}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Tapeti.Saga</RootNamespace>
|
||||
<AssemblyName>Tapeti.Saga</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ISaga.cs" />
|
||||
<Compile Include="ISagaProvider.cs" />
|
||||
<Compile Include="ISagaStore.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
@ -1,5 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using SimpleInjector;
|
||||
|
||||
namespace Tapeti.SimpleInjector
|
||||
{
|
||||
public class SimpleInjectorControllerFactory : IControllerFactory
|
||||
{
|
||||
private readonly Container container;
|
||||
|
||||
|
||||
public SimpleInjectorControllerFactory(Container container)
|
||||
{
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
|
||||
public object CreateController(Type controllerType)
|
||||
{
|
||||
return container.GetInstance(controllerType);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,16 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SimpleInjector;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Default;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti.SimpleInjector
|
||||
{
|
||||
public class SimpleInjectorDependencyResolver : IDependencyResolver
|
||||
public class SimpleInjectorDependencyResolver : IDependencyInjector
|
||||
{
|
||||
private readonly Container container;
|
||||
|
||||
public SimpleInjectorDependencyResolver(Container container, bool registerDefaults = true)
|
||||
public SimpleInjectorDependencyResolver(Container container)
|
||||
{
|
||||
this.container = container;
|
||||
|
||||
if (registerDefaults)
|
||||
RegisterDefaults();
|
||||
}
|
||||
|
||||
public T Resolve<T>() where T : class
|
||||
@ -24,33 +18,31 @@ namespace Tapeti.SimpleInjector
|
||||
return container.GetInstance<T>();
|
||||
}
|
||||
|
||||
|
||||
public SimpleInjectorDependencyResolver RegisterDefaults()
|
||||
public object Resolve(Type type)
|
||||
{
|
||||
var currentRegistrations = container.GetCurrentRegistrations();
|
||||
|
||||
IfUnregistered<IControllerFactory, SimpleInjectorControllerFactory>(currentRegistrations);
|
||||
IfUnregistered<IMessageSerializer, DefaultMessageSerializer>(currentRegistrations);
|
||||
IfUnregistered<IRoutingKeyStrategy, DefaultRoutingKeyStrategy>(currentRegistrations);
|
||||
|
||||
return this;
|
||||
return container.GetInstance(type);
|
||||
}
|
||||
|
||||
|
||||
public SimpleInjectorDependencyResolver RegisterAllControllers(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(QueueAttribute))))
|
||||
container.Register(type);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
private void IfUnregistered<TService, TImplementation>(IEnumerable<InstanceProducer> currentRegistrations) where TService : class where TImplementation: class, TService
|
||||
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||
{
|
||||
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
||||
if (!currentRegistrations.Any(ip => ip.ServiceType == typeof(TService)))
|
||||
if (!container.GetCurrentRegistrations().Any(ip => ip.ServiceType == typeof(TService)))
|
||||
container.Register<TService, TImplementation>();
|
||||
}
|
||||
|
||||
|
||||
public void RegisterPublisher(Func<IPublisher> publisher)
|
||||
{
|
||||
// ReSharper disable once SimplifyLinqExpression - still not a fan of negative predicates
|
||||
if (!container.GetCurrentRegistrations().Any(ip => ip.ServiceType == typeof(IPublisher)))
|
||||
container.Register(publisher);
|
||||
}
|
||||
|
||||
|
||||
public void RegisterController(Type type)
|
||||
{
|
||||
container.Register(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,6 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="SimpleInjectorControllerFactory.cs" />
|
||||
<Compile Include="SimpleInjectorDependencyResolver.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
@ -50,38 +50,47 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Annotations\ExchangeAttribute.cs" />
|
||||
<Compile Include="Annotations\QueueAttribute.cs" />
|
||||
<Compile Include="Annotations\MessageControllerAttribute.cs" />
|
||||
<Compile Include="Annotations\StaticQueueAttribute.cs" />
|
||||
<Compile Include="Annotations\DynamicQueueAttribute.cs" />
|
||||
<Compile Include="Connection\TapetiConsumer.cs" />
|
||||
<Compile Include="Connection\TapetiPublisher.cs" />
|
||||
<Compile Include="Connection\TapetiSubscriber.cs" />
|
||||
<Compile Include="Connection\TapetiWorker.cs" />
|
||||
<Compile Include="Default\ConsoleLogger.cs" />
|
||||
<Compile Include="Default\DevNullLogger.cs" />
|
||||
<Compile Include="Helpers\ConsoleHelper.cs" />
|
||||
<Compile Include="Helpers\MiddlewareHelper.cs" />
|
||||
<Compile Include="IConnection.cs" />
|
||||
<Compile Include="ILogger.cs" />
|
||||
<Compile Include="TapetiConnectionExtensions.cs" />
|
||||
<Compile Include="Config\IMessageContext.cs" />
|
||||
<Compile Include="Default\BindingBufferStop.cs" />
|
||||
<Compile Include="Config\IMessageMiddleware.cs" />
|
||||
<Compile Include="Config\IMiddlewareBundle.cs" />
|
||||
<Compile Include="Config\IConfig.cs" />
|
||||
<Compile Include="MessageController.cs" />
|
||||
<Compile Include="Config\IBindingMiddleware.cs" />
|
||||
<Compile Include="TapetiConnectionParams.cs" />
|
||||
<Compile Include="TapetiConfig.cs" />
|
||||
<Compile Include="TapetiTypes.cs" />
|
||||
<Compile Include="Tasks\SingleThreadTaskQueue.cs" />
|
||||
<Compile Include="Default\DefaultControllerFactory.cs" />
|
||||
<Compile Include="Default\DefaultDependencyResolver.cs" />
|
||||
<Compile Include="Default\DefaultMessageSerializer.cs" />
|
||||
<Compile Include="Default\DefaultRoutingKeyStrategy.cs" />
|
||||
<Compile Include="IControllerFactory.cs" />
|
||||
<Compile Include="IDependencyResolver.cs" />
|
||||
<Compile Include="IMessageSerializer.cs" />
|
||||
<Compile Include="IPublisher.cs" />
|
||||
<Compile Include="IRoutingKeyStrategy.cs" />
|
||||
<Compile Include="IQueueRegistration.cs" />
|
||||
<Compile Include="ISubscriber.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Registration\AbstractControllerRegistration.cs" />
|
||||
<Compile Include="Registration\ControllerDynamicQueueRegistration.cs" />
|
||||
<Compile Include="Registration\ControllerQueueRegistration.cs" />
|
||||
<Compile Include="TapetiConnection.cs" />
|
||||
<Compile Include="Config\IBindingContext.cs" />
|
||||
<Compile Include="Default\DependencyResolverBinding.cs" />
|
||||
<Compile Include="Default\MessageBinding.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.SimpleInjector", "Ta
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{90559950-1B32-4119-A78E-517E2C71EE23}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Saga", "Tapeti.Saga\Tapeti.Saga.csproj", "{F84AD920-D5A1-455D-AED5-2542B3A47B85}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -27,6 +29,10 @@ Global
|
||||
{90559950-1B32-4119-A78E-517E2C71EE23}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{90559950-1B32-4119-A78E-517E2C71EE23}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{90559950-1B32-4119-A78E-517E2C71EE23}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F84AD920-D5A1-455D-AED5-2542B3A47B85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F84AD920-D5A1-455D-AED5-2542B3A47B85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F84AD920-D5A1-455D-AED5-2542B3A47B85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F84AD920-D5A1-455D-AED5-2542B3A47B85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
400
TapetiConfig.cs
Normal file
400
TapetiConfig.cs
Normal file
@ -0,0 +1,400 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public class TopologyConfigurationException : Exception
|
||||
{
|
||||
public TopologyConfigurationException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
public delegate Task<object> MessageHandlerFunc(IMessageContext context, object message);
|
||||
|
||||
|
||||
public class TapetiConfig
|
||||
{
|
||||
private readonly Dictionary<string, List<Binding>> staticRegistrations = new Dictionary<string, List<Binding>>();
|
||||
private readonly Dictionary<Type, List<Binding>> dynamicRegistrations = new Dictionary<Type, List<Binding>>();
|
||||
|
||||
private readonly List<IBindingMiddleware> bindingMiddleware = new List<IBindingMiddleware>();
|
||||
private readonly List<IMessageMiddleware> messageMiddleware = new List<IMessageMiddleware>();
|
||||
|
||||
private readonly string exchange;
|
||||
private readonly IDependencyResolver dependencyResolver;
|
||||
|
||||
|
||||
public TapetiConfig(string exchange, IDependencyResolver dependencyResolver)
|
||||
{
|
||||
this.exchange = exchange;
|
||||
this.dependencyResolver = dependencyResolver;
|
||||
|
||||
Use(new BindingBufferStop());
|
||||
Use(new DependencyResolverBinding(dependencyResolver));
|
||||
Use(new MessageBinding());
|
||||
}
|
||||
|
||||
|
||||
public IConfig Build()
|
||||
{
|
||||
var dependencyInjector = dependencyResolver as IDependencyInjector;
|
||||
if (dependencyInjector != null)
|
||||
{
|
||||
if (ConsoleHelper.IsAvailable())
|
||||
dependencyInjector.RegisterDefault<ILogger, ConsoleLogger>();
|
||||
else
|
||||
dependencyInjector.RegisterDefault<ILogger, DevNullLogger>();
|
||||
|
||||
dependencyInjector.RegisterDefault<IMessageSerializer, DefaultMessageSerializer>();
|
||||
dependencyInjector.RegisterDefault<IRoutingKeyStrategy, DefaultRoutingKeyStrategy>();
|
||||
}
|
||||
|
||||
var queues = new List<IQueue>();
|
||||
queues.AddRange(staticRegistrations.Select(qb => new Queue(new QueueInfo { Dynamic = false, Name = qb.Key }, qb.Value)));
|
||||
|
||||
// Group all bindings with the same index into queues, this will
|
||||
// ensure each message type is unique on their queue
|
||||
var dynamicBindings = new List<List<Binding>>();
|
||||
foreach (var bindings in dynamicRegistrations.Values)
|
||||
{
|
||||
while (dynamicBindings.Count < bindings.Count)
|
||||
dynamicBindings.Add(new List<Binding>());
|
||||
|
||||
for (var bindingIndex = 0; bindingIndex < bindings.Count; bindingIndex++)
|
||||
dynamicBindings[bindingIndex].Add(bindings[bindingIndex]);
|
||||
}
|
||||
|
||||
queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true }, bl)));
|
||||
|
||||
return new Config(exchange, dependencyResolver, messageMiddleware, queues);
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IBindingMiddleware handler)
|
||||
{
|
||||
bindingMiddleware.Add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IMessageMiddleware handler)
|
||||
{
|
||||
messageMiddleware.Add(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig Use(IMiddlewareBundle bundle)
|
||||
{
|
||||
foreach (var middleware in bundle.GetContents(dependencyResolver))
|
||||
{
|
||||
// ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull
|
||||
if (middleware is IBindingMiddleware)
|
||||
Use((IBindingMiddleware) middleware);
|
||||
else if (middleware is IMessageMiddleware)
|
||||
Use((IMessageMiddleware)middleware);
|
||||
else
|
||||
throw new ArgumentException($"Unsupported middleware implementation: {middleware.GetType().Name}");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConfig RegisterController(Type controller)
|
||||
{
|
||||
var controllerQueueInfo = GetQueueInfo(controller);
|
||||
|
||||
foreach (var method in controller.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object))
|
||||
.Select(m => (MethodInfo)m))
|
||||
{
|
||||
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo;
|
||||
if (!methodQueueInfo.IsValid)
|
||||
throw new TopologyConfigurationException($"Method {method.Name} or controller {controller.Name} requires a queue attribute");
|
||||
|
||||
var context = new BindingContext(method.GetParameters().Select(p => new BindingParameter(p)).ToList());
|
||||
var messageHandler = GetMessageHandler(context, method);
|
||||
|
||||
var handlerInfo = new Binding
|
||||
{
|
||||
Controller = controller,
|
||||
Method = method,
|
||||
QueueInfo = methodQueueInfo,
|
||||
MessageClass = context.MessageClass,
|
||||
MessageHandler = messageHandler
|
||||
};
|
||||
|
||||
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
||||
AddDynamicRegistration(context, handlerInfo);
|
||||
else
|
||||
AddStaticRegistration(context, handlerInfo);
|
||||
}
|
||||
|
||||
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.GetCallingAssembly());
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc GetMessageHandler(IBindingContext context, MethodInfo method)
|
||||
{
|
||||
MiddlewareHelper.Go(bindingMiddleware, (handler, next) => handler.Handle(context, next));
|
||||
|
||||
if (context.MessageClass == null)
|
||||
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} does not resolve to a message class");
|
||||
|
||||
|
||||
var invalidBindings = context.Parameters.Where(p => !p.HasBinding).ToList();
|
||||
|
||||
// ReSharper disable once InvertIf - doesn't make the flow clearer imo
|
||||
if (invalidBindings.Count > 0)
|
||||
{
|
||||
var parameterNames = string.Join(", ", invalidBindings.Select(p => p.Info.Name));
|
||||
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
|
||||
}
|
||||
|
||||
return WrapMethod(method, context.Parameters.Select(p => ((IBindingParameterAccess)p).GetBinding()));
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||
{
|
||||
if (method.ReturnType == typeof(void))
|
||||
return WrapNullMethod(method, parameters);
|
||||
|
||||
if (method.ReturnType == typeof(Task))
|
||||
return WrapTaskMethod(method, parameters);
|
||||
|
||||
if (method.ReturnType == typeof(Task<>))
|
||||
{
|
||||
var genericArguments = method.GetGenericArguments();
|
||||
if (genericArguments.Length != 1)
|
||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} must have exactly one generic argument to Task<>");
|
||||
|
||||
if (!genericArguments[0].IsClass)
|
||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} must have an object generic argument to Task<>");
|
||||
|
||||
return WrapGenericTaskMethod(method, parameters);
|
||||
}
|
||||
|
||||
if (method.ReturnType.IsClass)
|
||||
return WrapObjectMethod(method, parameters);
|
||||
|
||||
throw new ArgumentException($"Method {method.Name} in controller {method.DeclaringType?.Name} has an invalid return type");
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc WrapNullMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||
{
|
||||
return (context, message) =>
|
||||
{
|
||||
method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||
return Task.FromResult<object>(null);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc WrapTaskMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||
{
|
||||
return async (context, message) =>
|
||||
{
|
||||
await (Task)method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||
return Task.FromResult<object>(null);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc WrapGenericTaskMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||
{
|
||||
return (context, message) =>
|
||||
{
|
||||
return (Task<object>)method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected MessageHandlerFunc WrapObjectMethod(MethodInfo method, IEnumerable<ValueFactory> parameters)
|
||||
{
|
||||
return (context, message) =>
|
||||
{
|
||||
return Task.FromResult(method.Invoke(context.Controller, parameters.Select(p => p(context)).ToArray()));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
protected void AddStaticRegistration(IBindingContext context, Binding binding)
|
||||
{
|
||||
if (staticRegistrations.ContainsKey(binding.QueueInfo.Name))
|
||||
{
|
||||
var existing = staticRegistrations[binding.QueueInfo.Name];
|
||||
|
||||
// Technically we could easily do multicasting, but it complicates exception handling and requeueing
|
||||
if (existing.Any(h => h.MessageClass == binding.MessageClass))
|
||||
throw new TopologyConfigurationException($"Multiple handlers for message class {binding.MessageClass.Name} in queue {binding.QueueInfo.Name}");
|
||||
|
||||
existing.Add(binding);
|
||||
}
|
||||
else
|
||||
staticRegistrations.Add(binding.QueueInfo.Name, new List<Binding> { binding });
|
||||
}
|
||||
|
||||
|
||||
protected void AddDynamicRegistration(IBindingContext context, Binding binding)
|
||||
{
|
||||
if (dynamicRegistrations.ContainsKey(context.MessageClass))
|
||||
dynamicRegistrations[context.MessageClass].Add(binding);
|
||||
else
|
||||
dynamicRegistrations.Add(context.MessageClass, new List<Binding> { binding });
|
||||
}
|
||||
|
||||
|
||||
protected QueueInfo GetQueueInfo(MemberInfo member)
|
||||
{
|
||||
var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
|
||||
var staticQueueAttribute = member.GetCustomAttribute<StaticQueueAttribute>();
|
||||
|
||||
if (dynamicQueueAttribute != null && staticQueueAttribute != null)
|
||||
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on {member.Name}");
|
||||
|
||||
if (dynamicQueueAttribute != null)
|
||||
return new QueueInfo { Dynamic = true };
|
||||
|
||||
if (staticQueueAttribute != null)
|
||||
return new QueueInfo { Dynamic = false, Name = staticQueueAttribute.Name };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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 string Exchange { get; }
|
||||
public IDependencyResolver DependencyResolver { get; }
|
||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||
public IEnumerable<IQueue> Queues { get; }
|
||||
|
||||
|
||||
public Config(string exchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
|
||||
{
|
||||
Exchange = exchange;
|
||||
DependencyResolver = dependencyResolver;
|
||||
MessageMiddleware = messageMiddleware;
|
||||
Queues = queues;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class Queue : IQueue
|
||||
{
|
||||
public bool Dynamic { get; }
|
||||
public string Name { get; }
|
||||
public IEnumerable<IBinding> Bindings { get; }
|
||||
|
||||
|
||||
public Queue(QueueInfo queue, IEnumerable<IBinding> bindings)
|
||||
{
|
||||
Dynamic = queue.Dynamic.GetValueOrDefault();
|
||||
Name = queue.Name;
|
||||
Bindings = bindings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class Binding : IBinding
|
||||
{
|
||||
public Type Controller { get; set; }
|
||||
public MethodInfo Method { get; set; }
|
||||
public Type MessageClass { get; set; }
|
||||
|
||||
public QueueInfo QueueInfo { get; set; }
|
||||
public MessageHandlerFunc MessageHandler { get; set; }
|
||||
|
||||
|
||||
public bool Accept(object message)
|
||||
{
|
||||
return message.GetType() == MessageClass;
|
||||
}
|
||||
|
||||
|
||||
public Task<object> Invoke(IMessageContext context, object message)
|
||||
{
|
||||
return MessageHandler(context, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal interface IBindingParameterAccess
|
||||
{
|
||||
ValueFactory GetBinding();
|
||||
}
|
||||
|
||||
|
||||
internal class BindingContext : IBindingContext
|
||||
{
|
||||
public Type MessageClass { get; set; }
|
||||
public IReadOnlyList<IBindingParameter> Parameters { get; }
|
||||
|
||||
|
||||
public BindingContext(IReadOnlyList<IBindingParameter> parameters)
|
||||
{
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Config;
|
||||
using Tapeti.Connection;
|
||||
using Tapeti.Default;
|
||||
using Tapeti.Registration;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public class TapetiConnection : IDisposable
|
||||
{
|
||||
private readonly IConfig config;
|
||||
public TapetiConnectionParams Params { get; set; }
|
||||
|
||||
public string PublishExchange { get; set; } = "";
|
||||
public string SubscribeExchange { get; set; } = "";
|
||||
|
||||
|
||||
public IDependencyResolver DependencyResolver
|
||||
{
|
||||
get { return dependencyResolver ?? (dependencyResolver = new DefaultDependencyResolver(GetPublisher)); }
|
||||
set { dependencyResolver = value; }
|
||||
}
|
||||
|
||||
|
||||
private IDependencyResolver dependencyResolver;
|
||||
private readonly Lazy<List<IQueueRegistration>> registrations = new Lazy<List<IQueueRegistration>>();
|
||||
private readonly Lazy<TapetiWorker> worker;
|
||||
|
||||
|
||||
public TapetiConnection()
|
||||
public TapetiConnection(IConfig config)
|
||||
{
|
||||
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(
|
||||
DependencyResolver.Resolve<IMessageSerializer>(),
|
||||
DependencyResolver.Resolve<IRoutingKeyStrategy>())
|
||||
this.config = config;
|
||||
(config.DependencyResolver as IDependencyInjector)?.RegisterPublisher(GetPublisher);
|
||||
|
||||
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config.DependencyResolver, config.MessageMiddleware)
|
||||
{
|
||||
ConnectionParams = Params ?? new TapetiConnectionParams(),
|
||||
PublishExchange = PublishExchange
|
||||
Exchange = config.Exchange
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public TapetiConnection WithDependencyResolver(IDependencyResolver resolver)
|
||||
{
|
||||
dependencyResolver = resolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConnection RegisterController(Type type)
|
||||
{
|
||||
var queueAttribute = type.GetCustomAttribute<QueueAttribute>();
|
||||
if (queueAttribute == null)
|
||||
throw new ArgumentException("Queue attribute required on class", nameof(type));
|
||||
|
||||
if (queueAttribute.Dynamic)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(queueAttribute.Name))
|
||||
throw new ArgumentException("Dynamic queue attributes must not have a Name");
|
||||
|
||||
registrations.Value.Add(new ControllerDynamicQueueRegistration(
|
||||
DependencyResolver.Resolve<IControllerFactory>,
|
||||
DependencyResolver.Resolve<IRoutingKeyStrategy>,
|
||||
type, SubscribeExchange));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(queueAttribute.Name))
|
||||
throw new ArgumentException("Non-dynamic queue attribute must have a Name");
|
||||
|
||||
registrations.Value.Add(new ControllerQueueRegistration(
|
||||
DependencyResolver.Resolve<IControllerFactory>,
|
||||
type, SubscribeExchange, queueAttribute.Name));
|
||||
}
|
||||
|
||||
(DependencyResolver as IDependencyInjector)?.RegisterController(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ISubscriber> Subscribe()
|
||||
{
|
||||
if (!registrations.IsValueCreated || registrations.Value.Count == 0)
|
||||
throw new ArgumentException("No controllers registered");
|
||||
|
||||
var subscriber = new TapetiSubscriber(worker.Value);
|
||||
await subscriber.BindQueues(registrations.Value);
|
||||
var subscriber = new TapetiSubscriber(() => worker.Value);
|
||||
await subscriber.BindQueues(config.Queues);
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
@ -93,7 +37,7 @@ namespace Tapeti
|
||||
|
||||
public IPublisher GetPublisher()
|
||||
{
|
||||
return new TapetiPublisher(worker.Value);
|
||||
return new TapetiPublisher(() => worker.Value);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Tapeti.Annotations;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public static class TapetiConnectionExtensions
|
||||
{
|
||||
public static TapetiConnection RegisterAllControllers(this TapetiConnection connection, Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(QueueAttribute))))
|
||||
connection.RegisterController(type);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
public static TapetiConnection RegisterAllControllers(this TapetiConnection connection)
|
||||
{
|
||||
return RegisterAllControllers(connection, Assembly.GetCallingAssembly());
|
||||
}
|
||||
}
|
||||
}
|
91
Test/MarcoController.cs
Normal file
91
Test/MarcoController.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using Microsoft.SqlServer.Server;
|
||||
using Tapeti;
|
||||
using Tapeti.Annotations;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[DynamicQueue]
|
||||
public class MarcoController : MessageController
|
||||
{
|
||||
private readonly IPublisher publisher;
|
||||
|
||||
|
||||
public MarcoController(IPublisher publisher/*, ISagaProvider sagaProvider*/)
|
||||
{
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
|
||||
//[StaticQueue("test")]
|
||||
public PoloMessage Marco(MarcoMessage message)
|
||||
{
|
||||
/*
|
||||
using (sagaProvider.Begin<MarcoState>(new MarcoState
|
||||
{
|
||||
...
|
||||
}))
|
||||
{
|
||||
//publisher.Publish(new PoloColorRequest(), saga, PoloColorResponse1);
|
||||
//publisher.Publish(new PoloColorRequest(), saga, callID = "tweede");
|
||||
|
||||
// Saga refcount = 2
|
||||
}
|
||||
*/
|
||||
|
||||
return new PoloMessage(); ;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
[CallID("eerste")]
|
||||
Implicit:
|
||||
|
||||
using (sagaProvider.Continue(correlatieID))
|
||||
{
|
||||
saga refcount--;
|
||||
public void PoloColorResponse1(PoloColorResponse message, ISaga<MarcoState> saga)
|
||||
{
|
||||
|
||||
saga.State == MarcoState
|
||||
|
||||
|
||||
|
||||
state.Color = message.Color;
|
||||
|
||||
if (state.Complete)
|
||||
{
|
||||
publisher.Publish(new PoloMessage());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public void Polo(PoloMessage message)
|
||||
{
|
||||
Console.WriteLine("Polo!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class MarcoMessage
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public class PoloMessage
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public class PoloColorRequest
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class PoloColorResponse
|
||||
{
|
||||
|
||||
}
|
||||
}
|
37
Test/MarcoEmitter.cs
Normal file
37
Test/MarcoEmitter.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MarcoEmitter
|
||||
{
|
||||
private readonly IPublisher publisher;
|
||||
|
||||
|
||||
public MarcoEmitter(IPublisher publisher)
|
||||
{
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
var concurrent = new SemaphoreSlim(20);
|
||||
|
||||
//for (var x = 0; x < 5000; x++)
|
||||
while (true)
|
||||
{
|
||||
await concurrent.WaitAsync();
|
||||
try
|
||||
{
|
||||
await publisher.Publish(new MarcoMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
concurrent.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,28 +10,25 @@ namespace Test
|
||||
private static void Main()
|
||||
{
|
||||
var container = new Container();
|
||||
container.Register<MarcoEmitter>();
|
||||
|
||||
using (var connection = new TapetiConnection
|
||||
{
|
||||
PublishExchange = "test",
|
||||
SubscribeExchange = "test"
|
||||
}
|
||||
.WithDependencyResolver(new SimpleInjectorDependencyResolver(container))
|
||||
.RegisterAllControllers(typeof(Program).Assembly))
|
||||
{
|
||||
container.Register(() => connection.GetPublisher());
|
||||
|
||||
var topology = new TapetiTopologyBuilder()
|
||||
.RegisterAllControllers()
|
||||
.Build();
|
||||
|
||||
using (var connection = new TapetiConnectionBuilder()
|
||||
.SetExchange("test")
|
||||
.SetDependencyResolver(new SimpleInjectorDependencyResolver(container))
|
||||
.SetTopology(topology)
|
||||
.Build())
|
||||
{
|
||||
Console.WriteLine("Subscribing...");
|
||||
connection.Subscribe().Wait();
|
||||
Console.WriteLine("Done!");
|
||||
|
||||
var publisher = connection.GetPublisher();
|
||||
|
||||
//for (var x = 0; x < 5000; x++)
|
||||
while(true)
|
||||
publisher.Publish(new MarcoMessage()).Wait();
|
||||
|
||||
//Console.ReadLine();
|
||||
var emitter = container.GetInstance<MarcoEmitter>();
|
||||
emitter.Run().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
|
@ -48,9 +48,10 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MarcoEmitter.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TestQueueController.cs" />
|
||||
<Compile Include="MarcoController.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti;
|
||||
using Tapeti.Annotations;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
//[Exchange("myexchange")]
|
||||
//[Queue("staticqueue")]
|
||||
[Queue]
|
||||
public class TestQueueController
|
||||
{
|
||||
private readonly IPublisher publisher;
|
||||
|
||||
|
||||
public TestQueueController(IPublisher publisher)
|
||||
{
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
|
||||
public async Task Marco(MarcoMessage message)
|
||||
{
|
||||
Console.WriteLine("Marco!");
|
||||
await publisher.Publish(new PoloMessage());
|
||||
}
|
||||
|
||||
|
||||
public void Polo(PoloMessage message)
|
||||
{
|
||||
Console.WriteLine("Polo!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class MarcoMessage
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public class PoloMessage
|
||||
{
|
||||
}
|
||||
}
|
17
Test/Visualizer.cs
Normal file
17
Test/Visualizer.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class Visualizer
|
||||
{
|
||||
public void VisualizeMarco()
|
||||
{
|
||||
Console.WriteLine("Marco!");
|
||||
}
|
||||
|
||||
public void VisualizePolo()
|
||||
{
|
||||
Console.WriteLine("Polo!");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user