Back to a working state
This commit is contained in:
parent
dfd480444e
commit
8f5160e860
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Tapeti.Config;
|
||||
|
||||
namespace Tapeti.Connection
|
||||
{
|
||||
@ -16,9 +17,9 @@ namespace Tapeti.Connection
|
||||
}
|
||||
|
||||
|
||||
public async Task BindQueues(IEnumerable<IQueueRegistration> registrations)
|
||||
public async Task BindQueues(IEnumerable<IQueue> queues)
|
||||
{
|
||||
await Task.WhenAll(registrations.Select(registration => workerFactory().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 RabbitMQ.Client.IConnection connection;
|
||||
private IModel channel;
|
||||
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,72 +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;
|
||||
private IPublisher publisher;
|
||||
|
||||
|
||||
public DefaultDependencyResolver()
|
||||
{
|
||||
controllerFactory = new Lazy<DefaultControllerFactory>(() => new DefaultControllerFactory(() => publisher));
|
||||
|
||||
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 RegisterPublisher(IPublisher value)
|
||||
{
|
||||
publisher = value;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface IControllerFactory
|
||||
{
|
||||
object CreateController(Type controllerType);
|
||||
}
|
||||
}
|
@ -5,12 +5,14 @@ namespace Tapeti
|
||||
public interface IDependencyResolver
|
||||
{
|
||||
T Resolve<T>() where T : class;
|
||||
object Resolve(Type type);
|
||||
}
|
||||
|
||||
|
||||
public interface IDependencyInjector : IDependencyResolver
|
||||
{
|
||||
void RegisterPublisher(IPublisher publisher);
|
||||
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
|
||||
{
|
||||
|
20
ITopology.cs
20
ITopology.cs
@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public interface ITopology
|
||||
{
|
||||
IEnumerable<IQueue> Queues();
|
||||
}
|
||||
|
||||
|
||||
public interface IQueue
|
||||
{
|
||||
IEnumerable<IBinding> Bindings();
|
||||
}
|
||||
|
||||
|
||||
public interface IBinding
|
||||
{
|
||||
}
|
||||
}
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,23 +1,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using SimpleInjector;
|
||||
using Tapeti.Annotations;
|
||||
using Tapeti.Default;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tapeti.SimpleInjector
|
||||
{
|
||||
public class SimpleInjectorDependencyResolver : IDependencyResolver, IDependencyInjector
|
||||
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
|
||||
@ -25,10 +18,25 @@ namespace Tapeti.SimpleInjector
|
||||
return container.GetInstance<T>();
|
||||
}
|
||||
|
||||
|
||||
public void RegisterPublisher(IPublisher publisher)
|
||||
public object Resolve(Type type)
|
||||
{
|
||||
IfUnregistered<IPublisher>(container.GetCurrentRegistrations(), () => container.RegisterSingleton(publisher));
|
||||
return container.GetInstance(type);
|
||||
}
|
||||
|
||||
|
||||
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
|
||||
{
|
||||
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -36,32 +44,5 @@ namespace Tapeti.SimpleInjector
|
||||
{
|
||||
container.Register(type);
|
||||
}
|
||||
|
||||
|
||||
public SimpleInjectorDependencyResolver RegisterDefaults()
|
||||
{
|
||||
var currentRegistrations = container.GetCurrentRegistrations();
|
||||
|
||||
IfUnregistered<IControllerFactory, SimpleInjectorControllerFactory>(currentRegistrations);
|
||||
IfUnregistered<IMessageSerializer, DefaultMessageSerializer>(currentRegistrations);
|
||||
IfUnregistered<IRoutingKeyStrategy, DefaultRoutingKeyStrategy>(currentRegistrations);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
private void IfUnregistered<TService, TImplementation>(IEnumerable<InstanceProducer> currentRegistrations) 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)))
|
||||
container.Register<TService, TImplementation>();
|
||||
}
|
||||
|
||||
private void IfUnregistered<TService>(IEnumerable<InstanceProducer> currentRegistrations, Action register) where TService : class
|
||||
{
|
||||
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
||||
if (!currentRegistrations.Any(ip => ip.ServiceType == typeof(TService)))
|
||||
register();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,6 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="SimpleInjectorControllerFactory.cs" />
|
||||
<Compile Include="SimpleInjectorDependencyResolver.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
@ -59,35 +59,38 @@
|
||||
<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="ITopology.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="TapetiConnectionBuilder.cs" />
|
||||
<Compile Include="Config\IBindingMiddleware.cs" />
|
||||
<Compile Include="TapetiConnectionParams.cs" />
|
||||
<Compile Include="TapetiTopologyBuilder.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.
|
||||
|
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,72 +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
|
||||
{
|
||||
if (dependencyResolver == null)
|
||||
DependencyResolver = new DefaultDependencyResolver();
|
||||
|
||||
return dependencyResolver;
|
||||
}
|
||||
set
|
||||
{
|
||||
dependencyResolver = value;
|
||||
|
||||
var dependencyInjector = value as IDependencyInjector;
|
||||
dependencyInjector?.RegisterPublisher(GetPublisher());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 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);
|
||||
await subscriber.BindQueues(config.Queues);
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public class TapetiConnectionBuilder
|
||||
{
|
||||
public IConnection Build()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public TapetiConnectionBuilder SetExchange(string exchange)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConnectionBuilder SetDependencyResolver(IDependencyResolver dependencyResolver)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiConnectionBuilder SetTopology(ITopology topology)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Tapeti.Annotations;
|
||||
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public class TopologyConfigurationException : Exception
|
||||
{
|
||||
public TopologyConfigurationException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
|
||||
public class TapetiTopologyBuilder
|
||||
{
|
||||
private readonly List<HandlerRegistration> registrations = new List<HandlerRegistration>();
|
||||
|
||||
|
||||
public ITopology Build()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public TapetiTopologyBuilder RegisterController(Type controller)
|
||||
{
|
||||
var controllerRegistration = GetAttributesRegistration(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))
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
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 TapetiTopologyBuilder RegisterAllControllers(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes().Where(t => t.IsDefined(typeof(MessageControllerAttribute))))
|
||||
RegisterController(type);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public TapetiTopologyBuilder RegisterAllControllers()
|
||||
{
|
||||
return RegisterAllControllers(Assembly.GetCallingAssembly());
|
||||
}
|
||||
|
||||
|
||||
protected HandlerRegistration GetAttributesRegistration(MemberInfo member)
|
||||
{
|
||||
var registration = new HandlerRegistration();
|
||||
|
||||
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)
|
||||
registration.Dynamic = true;
|
||||
else if (staticQueueAttribute != null)
|
||||
{
|
||||
registration.Dynamic = false;
|
||||
registration.QueueName = staticQueueAttribute.Name;
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
|
||||
protected class HandlerRegistration
|
||||
{
|
||||
public bool? Dynamic { get; set; }
|
||||
public string QueueName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected class Topology : ITopology
|
||||
{
|
||||
private readonly List<Queue> queues = new List<Queue>();
|
||||
|
||||
|
||||
public void Add(Queue queue)
|
||||
{
|
||||
queues.Add(queue);
|
||||
}
|
||||
|
||||
public IEnumerable<IQueue> Queues()
|
||||
{
|
||||
return queues;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class Queue : IQueue
|
||||
{
|
||||
private readonly List<Binding> bindings = new List<Binding>();
|
||||
|
||||
|
||||
public void Add(Binding binding)
|
||||
{
|
||||
bindings.Add(binding);
|
||||
}
|
||||
|
||||
public IEnumerable<IBinding> Bindings()
|
||||
{
|
||||
return bindings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected class Binding : IBinding
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
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