1
0
mirror of synced 2024-12-22 09:13:07 +01:00

Merge branch 'feature/multiqueuecontrollers' into develop

This commit is contained in:
Mark van Renswoude 2016-12-11 15:09:20 +01:00
commit 0c4f8a04f0
58 changed files with 1338 additions and 631 deletions

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

View File

@ -0,0 +1,9 @@
using System;
namespace Tapeti.Annotations
{
[AttributeUsage(AttributeTargets.Class)]
public class MessageControllerAttribute : Attribute
{
}
}

View File

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

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

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

View File

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

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Tapeti.Config
{
public interface IMiddlewareBundle
{
IEnumerable<object> GetContents(IDependencyResolver dependencyResolver);
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View 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
View File

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Tapeti
{
public interface IConnection : IDisposable
{
Task<ISubscriber> Subscribe();
}
}

View File

@ -1,9 +0,0 @@
using System;
namespace Tapeti
{
public interface IControllerFactory
{
object CreateController(Type controllerType);
}
}

View File

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

View File

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

View File

@ -1,6 +1,4 @@
using System;
namespace Tapeti
namespace Tapeti
{
public interface ISubscriber
{

16
MessageController.cs Normal file
View 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
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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")]

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,6 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="SimpleInjectorControllerFactory.cs" />
<Compile Include="SimpleInjectorDependencyResolver.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,17 @@
using System;
namespace Test
{
public class Visualizer
{
public void VisualizeMarco()
{
Console.WriteLine("Marco!");
}
public void VisualizePolo()
{
Console.WriteLine("Polo!");
}
}
}