Implemented consumer, serializer, routing key strategy and single-threaded task queue (first working version basically)
This commit is contained in:
parent
77de28d8b8
commit
646ba22e63
15
Annotations/ExchangeAttribute.cs
Normal file
15
Annotations/ExchangeAttribute.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Annotations
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public class ExchangeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public ExchangeAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Tapeti.Annotations
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public class MessageHandlerAttribute : Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,14 @@ namespace Tapeti.Annotations
|
|||||||
[AttributeUsage(AttributeTargets.Class)]
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
public class QueueAttribute : Attribute
|
public class QueueAttribute : Attribute
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = null;
|
public string Name { get; set; }
|
||||||
public bool Dynamic { get; set; } = false;
|
public bool Dynamic { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public QueueAttribute(string name = null)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Dynamic = (name == null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
Connection/TapetiConsumer.cs
Normal file
45
Connection/TapetiConsumer.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.Eventing.Reader;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
|
namespace Tapeti.Connection
|
||||||
|
{
|
||||||
|
public class TapetiConsumer : DefaultBasicConsumer
|
||||||
|
{
|
||||||
|
private readonly TapetiWorker worker;
|
||||||
|
private readonly IMessageSerializer messageSerializer;
|
||||||
|
private readonly IQueueRegistration queueRegistration;
|
||||||
|
|
||||||
|
|
||||||
|
public TapetiConsumer(TapetiWorker worker, IMessageSerializer messageSerializer, IQueueRegistration queueRegistration)
|
||||||
|
{
|
||||||
|
this.worker = worker;
|
||||||
|
this.messageSerializer = messageSerializer;
|
||||||
|
this.queueRegistration = queueRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey,
|
||||||
|
IBasicProperties properties, byte[] body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = messageSerializer.Deserialize(body, properties);
|
||||||
|
if (message == null)
|
||||||
|
throw new ArgumentException("Empty message");
|
||||||
|
|
||||||
|
if (queueRegistration.Accept(message))
|
||||||
|
queueRegistration.Visit(message);
|
||||||
|
else
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
@ -8,18 +9,15 @@ namespace Tapeti.Connection
|
|||||||
private readonly TapetiWorker worker;
|
private readonly TapetiWorker worker;
|
||||||
|
|
||||||
|
|
||||||
public TapetiSubscriber(TapetiWorker worker, IEnumerable<IMessageHandlerRegistration> registrations)
|
public TapetiSubscriber(TapetiWorker worker)
|
||||||
{
|
{
|
||||||
this.worker = worker;
|
this.worker = worker;
|
||||||
|
|
||||||
ApplyTopology(registrations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ApplyTopology(IEnumerable<IMessageHandlerRegistration> registrations)
|
public async Task BindQueues(IEnumerable<IQueueRegistration> registrations)
|
||||||
{
|
{
|
||||||
foreach (var registration in registrations)
|
await Task.WhenAll(registrations.Select(registration => worker.Subscribe(registration)).ToList());
|
||||||
worker.ApplyTopology(registration);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
|
using RabbitMQ.Client.Exceptions;
|
||||||
|
using RabbitMQ.Client.Framing;
|
||||||
|
using Tapeti.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
@ -11,48 +14,110 @@ namespace Tapeti.Connection
|
|||||||
public string VirtualHost { get; set; }
|
public string VirtualHost { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
public string PublishExchange { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IMessageSerializer messageSerializer;
|
||||||
|
private readonly IRoutingKeyStrategy routingKeyStrategy;
|
||||||
|
private readonly Lazy<SingleThreadTaskQueue> taskQueue = new Lazy<SingleThreadTaskQueue>();
|
||||||
private IConnection connection;
|
private IConnection connection;
|
||||||
private IModel channel;
|
private IModel channel;
|
||||||
private readonly Lazy<TaskQueue> taskQueue = new Lazy<TaskQueue>();
|
|
||||||
|
|
||||||
|
public TapetiWorker(IMessageSerializer messageSerializer, IRoutingKeyStrategy routingKeyStrategy)
|
||||||
|
{
|
||||||
|
this.messageSerializer = messageSerializer;
|
||||||
|
this.routingKeyStrategy = routingKeyStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Publish(object message)
|
public Task Publish(object message)
|
||||||
{
|
{
|
||||||
return taskQueue.Value.Add(() =>
|
return taskQueue.Value.Add(async () =>
|
||||||
{
|
{
|
||||||
//GetChannel().BasicPublish();
|
var properties = new BasicProperties();
|
||||||
});
|
var body = messageSerializer.Serialize(message, properties);
|
||||||
|
|
||||||
|
(await GetChannel())
|
||||||
|
.BasicPublish(PublishExchange, routingKeyStrategy.GetRoutingKey(message.GetType()), false,
|
||||||
|
properties, body);
|
||||||
|
}).Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ApplyTopology(IMessageHandlerRegistration registration)
|
public Task Subscribe(string queueName, IQueueRegistration queueRegistration)
|
||||||
{
|
{
|
||||||
registration.ApplyTopology(GetChannel());
|
return taskQueue.Value.Add(async () =>
|
||||||
|
{
|
||||||
|
(await GetChannel())
|
||||||
|
.BasicConsume(queueName, false, new TapetiConsumer(this, messageSerializer, queueRegistration));
|
||||||
|
}).Unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Subscribe(IQueueRegistration registration)
|
||||||
|
{
|
||||||
|
var queueName = await taskQueue.Value.Add(async () =>
|
||||||
|
registration.BindQueue(await GetChannel()))
|
||||||
|
.Unwrap();
|
||||||
|
|
||||||
|
await Subscribe(queueName, registration);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task Respond(ulong deliveryTag, ConsumeResponse response)
|
||||||
|
{
|
||||||
|
return taskQueue.Value.Add(async () =>
|
||||||
|
{
|
||||||
|
switch (response)
|
||||||
|
{
|
||||||
|
case ConsumeResponse.Ack:
|
||||||
|
(await GetChannel()).BasicAck(deliveryTag, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConsumeResponse.Nack:
|
||||||
|
(await GetChannel()).BasicNack(deliveryTag, false, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConsumeResponse.Requeue:
|
||||||
|
(await GetChannel()).BasicNack(deliveryTag, false, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Close()
|
public Task Close()
|
||||||
{
|
{
|
||||||
if (channel != null)
|
if (!taskQueue.IsValueCreated)
|
||||||
{
|
return Task.CompletedTask;
|
||||||
channel.Dispose();
|
|
||||||
channel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable once InvertIf
|
return taskQueue.Value.Add(() =>
|
||||||
if (connection != null)
|
|
||||||
{
|
{
|
||||||
connection.Dispose();
|
if (channel != null)
|
||||||
connection = null;
|
{
|
||||||
}
|
channel.Dispose();
|
||||||
|
channel = null;
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
// ReSharper disable once InvertIf
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
connection.Dispose();
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskQueue.Value.Dispose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private IModel GetChannel()
|
/// <remarks>
|
||||||
|
/// Only call this from a task in the taskQueue to ensure IModel is only used
|
||||||
|
/// by a single thread, as is recommended in the RabbitMQ .NET Client documentation.
|
||||||
|
/// </remarks>
|
||||||
|
private async Task<IModel> GetChannel()
|
||||||
{
|
{
|
||||||
if (channel != null)
|
if (channel != null)
|
||||||
return channel;
|
return channel;
|
||||||
@ -64,19 +129,26 @@ namespace Tapeti.Connection
|
|||||||
VirtualHost = VirtualHost,
|
VirtualHost = VirtualHost,
|
||||||
UserName = Username,
|
UserName = Username,
|
||||||
Password = Password,
|
Password = Password,
|
||||||
AutomaticRecoveryEnabled = true
|
AutomaticRecoveryEnabled = true,
|
||||||
|
RequestedHeartbeat = 30
|
||||||
};
|
};
|
||||||
|
|
||||||
connection = connectionFactory.CreateConnection();
|
while (true)
|
||||||
channel = connection.CreateModel();
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
connection = connectionFactory.CreateConnection();
|
||||||
|
channel = connection.CreateModel();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (BrokerUnreachableException)
|
||||||
|
{
|
||||||
|
await Task.Delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class ScheduledWorkItem
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
|
||||||
{
|
|
||||||
public class TaskQueue
|
|
||||||
{
|
|
||||||
private readonly object previousTaskLock = new object();
|
|
||||||
private Task previousTask = Task.CompletedTask;
|
|
||||||
|
|
||||||
|
|
||||||
public Task Add(Action action)
|
|
||||||
{
|
|
||||||
lock (previousTaskLock)
|
|
||||||
{
|
|
||||||
previousTask = previousTask.ContinueWith(t => action(), CancellationToken.None
|
|
||||||
, TaskContinuationOptions.None
|
|
||||||
, TaskScheduler.Default);
|
|
||||||
return previousTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,62 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class DefaultControllerFactory : IControllerFactory
|
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)
|
public object CreateController(Type controllerType)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
Default/DefaultDependencyResolver.cs
Normal file
46
Default/DefaultDependencyResolver.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public DefaultDependencyResolver(Func<IPublisher> publisherFactory)
|
||||||
|
{
|
||||||
|
controllerFactory = new Lazy<DefaultControllerFactory>(() => new DefaultControllerFactory(publisherFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return default(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void RegisterController(Type type)
|
||||||
|
{
|
||||||
|
controllerFactory.Value.RegisterController(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
Default/DefaultMessageSerializer.cs
Normal file
84
Default/DefaultMessageSerializer.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
public class DefaultMessageSerializer : IMessageSerializer
|
||||||
|
{
|
||||||
|
protected const string ContentType = "application/json";
|
||||||
|
protected const string ClassTypeHeader = "classType";
|
||||||
|
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, Type> deserializedTypeNames = new ConcurrentDictionary<string, Type>();
|
||||||
|
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
||||||
|
private readonly JsonSerializerSettings serializerSettings;
|
||||||
|
|
||||||
|
public DefaultMessageSerializer()
|
||||||
|
{
|
||||||
|
serializerSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
};
|
||||||
|
|
||||||
|
serializerSettings.Converters.Add(new StringEnumConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public byte[] Serialize(object message, IBasicProperties properties)
|
||||||
|
{
|
||||||
|
if (properties.Headers == null)
|
||||||
|
properties.Headers = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
var typeName = serializedTypeNames.GetOrAdd(message.GetType(), SerializeTypeName);
|
||||||
|
|
||||||
|
properties.Headers.Add(ClassTypeHeader, Encoding.UTF8.GetBytes(typeName));
|
||||||
|
properties.ContentType = ContentType;
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message, serializerSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public object Deserialize(byte[] body, IBasicProperties properties)
|
||||||
|
{
|
||||||
|
object typeName;
|
||||||
|
|
||||||
|
if (!properties.ContentType.Equals(ContentType))
|
||||||
|
throw new ArgumentException("content_type must be {ContentType}");
|
||||||
|
|
||||||
|
if (properties.Headers == null || !properties.Headers.TryGetValue(ClassTypeHeader, out typeName))
|
||||||
|
throw new ArgumentException($"{ClassTypeHeader} header not present");
|
||||||
|
|
||||||
|
var messageType = deserializedTypeNames.GetOrAdd(Encoding.UTF8.GetString((byte[])typeName), DeserializeTypeName);
|
||||||
|
return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), messageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public virtual Type DeserializeTypeName(string typeName)
|
||||||
|
{
|
||||||
|
var parts = typeName.Split(':');
|
||||||
|
if (parts.Length != 2)
|
||||||
|
throw new ArgumentException($"Invalid type name {typeName}");
|
||||||
|
|
||||||
|
var type = Type.GetType(parts[0] + "," + parts[1]);
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentException($"Unable to resolve type {typeName}");
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string SerializeTypeName(Type type)
|
||||||
|
{
|
||||||
|
var typeName = type.FullName + ":" + type.Assembly.GetName().Name;
|
||||||
|
if (typeName.Length > 255)
|
||||||
|
throw new ArgumentException($"Type name {typeName} exceeds AMQP 255 character limit");
|
||||||
|
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,59 @@
|
|||||||
namespace Tapeti.Default
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class DefaultRoutingKeyStrategy : IRoutingKeyStrategy
|
public class DefaultRoutingKeyStrategy : IRoutingKeyStrategy
|
||||||
{
|
{
|
||||||
|
private readonly ConcurrentDictionary<Type, string> routingKeyCache = new ConcurrentDictionary<Type, string>();
|
||||||
|
|
||||||
|
|
||||||
|
public string GetRoutingKey(Type messageType)
|
||||||
|
{
|
||||||
|
return routingKeyCache.GetOrAdd(messageType, BuildRoutingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual string BuildRoutingKey(Type messageType)
|
||||||
|
{
|
||||||
|
// Split PascalCase into dot-separated parts. If the class name ends in "Message" leave that out.
|
||||||
|
var words = SplitUpperCase(messageType.Name);
|
||||||
|
|
||||||
|
if (words.Count > 1 && words.Last().Equals("Message", StringComparison.InvariantCulture))
|
||||||
|
words.RemoveAt(words.Count - 1);
|
||||||
|
|
||||||
|
return string.Join(".", words.Select(s => s.ToLower()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static List<string> SplitUpperCase(string source)
|
||||||
|
{
|
||||||
|
var words = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(source))
|
||||||
|
return words;
|
||||||
|
|
||||||
|
var wordStartIndex = 0;
|
||||||
|
|
||||||
|
var letters = source.ToCharArray();
|
||||||
|
var previousChar = char.MinValue;
|
||||||
|
|
||||||
|
// Intentionally skip the first character
|
||||||
|
for (var charIndex = 1; charIndex < letters.Length; charIndex++)
|
||||||
|
{
|
||||||
|
if (char.IsUpper(letters[charIndex]) && !char.IsWhiteSpace(previousChar))
|
||||||
|
{
|
||||||
|
words.Add(new string(letters, wordStartIndex, charIndex - wordStartIndex));
|
||||||
|
wordStartIndex = charIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousChar = letters[charIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
words.Add(new string(letters, wordStartIndex, letters.Length - wordStartIndex));
|
||||||
|
return words;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
IDependencyResolver.cs
Normal file
15
IDependencyResolver.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
public interface IDependencyResolver
|
||||||
|
{
|
||||||
|
T Resolve<T>() where T : class;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface IDependencyInjector : IDependencyResolver
|
||||||
|
{
|
||||||
|
void RegisterController(Type type);
|
||||||
|
}
|
||||||
|
}
|
10
IMessageSerializer.cs
Normal file
10
IMessageSerializer.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
public interface IMessageSerializer
|
||||||
|
{
|
||||||
|
byte[] Serialize(object message, IBasicProperties properties);
|
||||||
|
object Deserialize(byte[] body, IBasicProperties properties);
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,9 @@ using RabbitMQ.Client;
|
|||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
public interface IMessageHandlerRegistration
|
public interface IQueueRegistration
|
||||||
{
|
{
|
||||||
void ApplyTopology(IModel channel);
|
string BindQueue(IModel channel);
|
||||||
|
|
||||||
bool Accept(object message);
|
bool Accept(object message);
|
||||||
Task Visit(object message);
|
Task Visit(object message);
|
@ -1,6 +1,9 @@
|
|||||||
namespace Tapeti
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
public interface IRoutingKeyStrategy
|
public interface IRoutingKeyStrategy
|
||||||
{
|
{
|
||||||
|
string GetRoutingKey(Type messageType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Tapeti
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
public interface ISubscriber
|
public interface ISubscriber
|
||||||
{
|
{
|
||||||
|
@ -10,59 +10,96 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
using MessageHandlerAction = Func<object, Task>;
|
using MessageHandlerAction = Func<object, Task>;
|
||||||
|
|
||||||
public abstract class AbstractControllerRegistration : IMessageHandlerRegistration
|
public struct MessageHandler
|
||||||
{
|
{
|
||||||
private readonly IControllerFactory controllerFactory;
|
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 Type controllerType;
|
||||||
private readonly Dictionary<Type, List<MessageHandlerAction>> messageHandlers = new Dictionary<Type, List<MessageHandlerAction>>();
|
private readonly string defaultExchange;
|
||||||
|
private readonly Dictionary<Type, List<MessageHandler>> messageHandlers = new Dictionary<Type, List<MessageHandler>>();
|
||||||
|
|
||||||
|
|
||||||
protected AbstractControllerRegistration(IControllerFactory controllerFactory, Type controllerType)
|
protected AbstractControllerRegistration(Func<IControllerFactory> controllerFactoryFactory, Type controllerType, string defaultExchange)
|
||||||
{
|
{
|
||||||
this.controllerFactory = controllerFactory;
|
this.controllerFactoryFactory = controllerFactoryFactory;
|
||||||
this.controllerType = controllerType;
|
this.controllerType = controllerType;
|
||||||
|
this.defaultExchange = defaultExchange;
|
||||||
|
|
||||||
// ReSharper disable once VirtualMemberCallInConstructor - I know. What do you think this is, C++?
|
// ReSharper disable once VirtualMemberCallInConstructor - I know. What do you think this is, C++?
|
||||||
GetMessageHandlers((type, handler) =>
|
GetMessageHandlers(controllerType, (type, handler) =>
|
||||||
{
|
{
|
||||||
if (!messageHandlers.ContainsKey(type))
|
if (!messageHandlers.ContainsKey(type))
|
||||||
messageHandlers.Add(type, new List<MessageHandlerAction> { handler });
|
messageHandlers.Add(type, new List<MessageHandler> { handler });
|
||||||
else
|
else
|
||||||
messageHandlers[type].Add(handler);
|
messageHandlers[type].Add(handler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected virtual void GetMessageHandlers(Action<Type, MessageHandlerAction> add)
|
protected virtual void GetMessageHandlers(Type type, Action<Type, MessageHandler> add)
|
||||||
{
|
{
|
||||||
foreach (var method in GetType().GetMembers()
|
foreach (var method in type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
|
||||||
.Where(m => m.MemberType == MemberTypes.Method && m.IsDefined(typeof(MessageHandlerAttribute), true))
|
.Where(m => m.MemberType == MemberTypes.Method && m.DeclaringType != typeof(object))
|
||||||
.Select(m => (MethodInfo)m))
|
.Select(m => (MethodInfo)m))
|
||||||
{
|
{
|
||||||
var parameters = method.GetParameters();
|
Type messageType;
|
||||||
|
var messageHandler = GetMessageHandler(method, out messageType);
|
||||||
|
|
||||||
if (parameters.Length != 1 || !parameters[0].ParameterType.IsClass)
|
add(messageType, messageHandler);
|
||||||
throw new ArgumentException($"Method {0} does not have a single object parameter", method.Name);
|
|
||||||
|
|
||||||
var messageType = parameters[0].ParameterType;
|
|
||||||
|
|
||||||
if (method.ReturnType == typeof(void))
|
|
||||||
add(messageType, CreateSyncMessageHandler(method));
|
|
||||||
else if (method.ReturnType == typeof(Task))
|
|
||||||
add(messageType, CreateAsyncMessageHandler(method));
|
|
||||||
else
|
|
||||||
throw new ArgumentException($"Method {0} needs to return void or a Task", method.Name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
protected IEnumerable<Type> GetMessageTypes()
|
||||||
{
|
{
|
||||||
return messageHandlers.Keys;
|
return messageHandlers.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public abstract void ApplyTopology(IModel channel);
|
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)
|
public bool Accept(object message)
|
||||||
@ -75,7 +112,7 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
var registeredHandlers = messageHandlers[message.GetType()];
|
var registeredHandlers = messageHandlers[message.GetType()];
|
||||||
if (registeredHandlers != null)
|
if (registeredHandlers != null)
|
||||||
return Task.WhenAll(registeredHandlers.Select(messageHandler => messageHandler(message)));
|
return Task.WhenAll(registeredHandlers.Select(messageHandler => messageHandler.Action(message)));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@ -85,7 +122,7 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
return message =>
|
return message =>
|
||||||
{
|
{
|
||||||
var controller = controllerFactory.CreateController(controllerType);
|
var controller = controllerFactoryFactory().CreateController(controllerType);
|
||||||
method.Invoke(controller, new[] { message });
|
method.Invoke(controller, new[] { message });
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -97,7 +134,7 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
return message =>
|
return message =>
|
||||||
{
|
{
|
||||||
var controller = controllerFactory.CreateController(controllerType);
|
var controller = controllerFactoryFactory().CreateController(controllerType);
|
||||||
return (Task)method.Invoke(controller, new[] { message });
|
return (Task)method.Invoke(controller, new[] { message });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,27 +5,29 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
public class ControllerDynamicQueueRegistration : AbstractControllerRegistration
|
public class ControllerDynamicQueueRegistration : AbstractControllerRegistration
|
||||||
{
|
{
|
||||||
private readonly IRoutingKeyStrategy routingKeyStrategy;
|
private readonly Func<IRoutingKeyStrategy> routingKeyStrategyFactory;
|
||||||
|
|
||||||
|
|
||||||
public ControllerDynamicQueueRegistration(IControllerFactory controllerFactory, IRoutingKeyStrategy routingKeyStrategy, Type controllerType)
|
public ControllerDynamicQueueRegistration(Func<IControllerFactory> controllerFactoryFactory, Func<IRoutingKeyStrategy> routingKeyStrategyFactory, Type controllerType, string defaultExchange)
|
||||||
: base(controllerFactory, controllerType)
|
: base(controllerFactoryFactory, controllerType, defaultExchange)
|
||||||
{
|
{
|
||||||
this.routingKeyStrategy = routingKeyStrategy;
|
this.routingKeyStrategyFactory = routingKeyStrategyFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void ApplyTopology(IModel channel)
|
public override string BindQueue(IModel channel)
|
||||||
{
|
{
|
||||||
var queue = channel.QueueDeclare();
|
var queue = channel.QueueDeclare();
|
||||||
|
|
||||||
foreach (var messageType in GetMessageTypes())
|
foreach (var messageType in GetMessageTypes())
|
||||||
{
|
{
|
||||||
//TODO use routing key attribute(s) for method or use strategy
|
var routingKey = routingKeyStrategyFactory().GetRoutingKey(messageType);
|
||||||
//TODO use exchange attribute or default setting
|
|
||||||
|
|
||||||
//channel.QueueBind(queue.QueueName, );
|
foreach (var exchange in GetMessageExchanges(messageType))
|
||||||
|
channel.QueueBind(queue.QueueName, exchange, routingKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return queue.QueueName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,15 @@ namespace Tapeti.Registration
|
|||||||
{
|
{
|
||||||
private readonly string queueName;
|
private readonly string queueName;
|
||||||
|
|
||||||
public ControllerQueueRegistration(IControllerFactory controllerFactory, IRoutingKeyStrategy routingKeyStrategy, Type controllerType, string queueName) : base(controllerFactory, controllerType)
|
public ControllerQueueRegistration(Func<IControllerFactory> controllerFactoryFactory, Type controllerType, string defaultExchange, string queueName) : base(controllerFactoryFactory, controllerType, defaultExchange)
|
||||||
{
|
{
|
||||||
this.queueName = queueName;
|
this.queueName = queueName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void ApplyTopology(IModel channel)
|
public override string BindQueue(IModel channel)
|
||||||
{
|
{
|
||||||
channel.QueueDeclarePassive(queueName);
|
return channel.QueueDeclarePassive(queueName).QueueName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
Tapeti.SimpleInjector/Properties/AssemblyInfo.cs
Normal file
36
Tapeti.SimpleInjector/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("Tapeti.SimpleInjector")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
||||||
|
[assembly: AssemblyProduct("Tapeti.SimpleInjector")]
|
||||||
|
[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("d7ec6f86-eb3b-49c3-8fe7-6e8c1bb413a6")]
|
||||||
|
|
||||||
|
// 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")]
|
22
Tapeti.SimpleInjector/SimpleInjectorControllerFactory.cs
Normal file
22
Tapeti.SimpleInjector/SimpleInjectorControllerFactory.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs
Normal file
56
Tapeti.SimpleInjector/SimpleInjectorDependencyResolver.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
private readonly Container container;
|
||||||
|
|
||||||
|
public SimpleInjectorDependencyResolver(Container container, bool registerDefaults = true)
|
||||||
|
{
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
if (registerDefaults)
|
||||||
|
RegisterDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Resolve<T>() where T : class
|
||||||
|
{
|
||||||
|
return container.GetInstance<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SimpleInjectorDependencyResolver RegisterDefaults()
|
||||||
|
{
|
||||||
|
var currentRegistrations = container.GetCurrentRegistrations();
|
||||||
|
|
||||||
|
IfUnregistered<IControllerFactory, SimpleInjectorControllerFactory>(currentRegistrations);
|
||||||
|
IfUnregistered<IMessageSerializer, DefaultMessageSerializer>(currentRegistrations);
|
||||||
|
IfUnregistered<IRoutingKeyStrategy, DefaultRoutingKeyStrategy>(currentRegistrations);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// ReSharper disable once SimplifyLinqExpression - not a fan of negative predicates
|
||||||
|
if (!currentRegistrations.Any(ip => ip.ServiceType == typeof(TService)))
|
||||||
|
container.Register<TService, TImplementation>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj
Normal file
69
Tapeti.SimpleInjector/Tapeti.SimpleInjector.csproj
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?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>{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>Tapeti.SimpleInjector</RootNamespace>
|
||||||
|
<AssemblyName>Tapeti.SimpleInjector</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<TargetFrameworkProfile />
|
||||||
|
</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="SimpleInjector, Version=3.2.7.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\SimpleInjector.3.2.7\lib\net45\SimpleInjector.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
|
<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="SimpleInjectorControllerFactory.cs" />
|
||||||
|
<Compile Include="SimpleInjectorDependencyResolver.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Tapeti.csproj">
|
||||||
|
<Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project>
|
||||||
|
<Name>Tapeti</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</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>
|
4
Tapeti.SimpleInjector/packages.config
Normal file
4
Tapeti.SimpleInjector/packages.config
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="SimpleInjector" version="3.2.7" targetFramework="net452" />
|
||||||
|
</packages>
|
@ -31,6 +31,10 @@
|
|||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
<Reference Include="RabbitMQ.Client, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89e7d7c5feba84ce, processorArchitecture=MSIL">
|
<Reference Include="RabbitMQ.Client, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89e7d7c5feba84ce, processorArchitecture=MSIL">
|
||||||
<HintPath>packages\RabbitMQ.Client.4.1.1\lib\net451\RabbitMQ.Client.dll</HintPath>
|
<HintPath>packages\RabbitMQ.Client.4.1.1\lib\net451\RabbitMQ.Client.dll</HintPath>
|
||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
@ -45,18 +49,24 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Annotations\MessageHandlerAttribute.cs" />
|
<Compile Include="Annotations\ExchangeAttribute.cs" />
|
||||||
<Compile Include="Annotations\QueueAttribute.cs" />
|
<Compile Include="Annotations\QueueAttribute.cs" />
|
||||||
|
<Compile Include="Connection\TapetiConsumer.cs" />
|
||||||
<Compile Include="Connection\TapetiPublisher.cs" />
|
<Compile Include="Connection\TapetiPublisher.cs" />
|
||||||
<Compile Include="Connection\TapetiSubscriber.cs" />
|
<Compile Include="Connection\TapetiSubscriber.cs" />
|
||||||
<Compile Include="Connection\TapetiWorker.cs" />
|
<Compile Include="Connection\TapetiWorker.cs" />
|
||||||
<Compile Include="Connection\TaskQueue.cs" />
|
<Compile Include="TapetiTypes.cs" />
|
||||||
|
<Compile Include="Tasks\SingleThreadTaskQueue.cs" />
|
||||||
<Compile Include="Default\DefaultControllerFactory.cs" />
|
<Compile Include="Default\DefaultControllerFactory.cs" />
|
||||||
|
<Compile Include="Default\DefaultDependencyResolver.cs" />
|
||||||
|
<Compile Include="Default\DefaultMessageSerializer.cs" />
|
||||||
<Compile Include="Default\DefaultRoutingKeyStrategy.cs" />
|
<Compile Include="Default\DefaultRoutingKeyStrategy.cs" />
|
||||||
<Compile Include="IControllerFactory.cs" />
|
<Compile Include="IControllerFactory.cs" />
|
||||||
|
<Compile Include="IDependencyResolver.cs" />
|
||||||
|
<Compile Include="IMessageSerializer.cs" />
|
||||||
<Compile Include="IPublisher.cs" />
|
<Compile Include="IPublisher.cs" />
|
||||||
<Compile Include="IRoutingKeyStrategy.cs" />
|
<Compile Include="IRoutingKeyStrategy.cs" />
|
||||||
<Compile Include="IMessageHandlerRegistration.cs" />
|
<Compile Include="IQueueRegistration.cs" />
|
||||||
<Compile Include="ISubscriber.cs" />
|
<Compile Include="ISubscriber.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Registration\AbstractControllerRegistration.cs" />
|
<Compile Include="Registration\AbstractControllerRegistration.cs" />
|
||||||
|
12
Tapeti.sln
12
Tapeti.sln
@ -5,6 +5,10 @@ VisualStudioVersion = 14.0.25420.1
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti", "Tapeti.csproj", "{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti", "Tapeti.csproj", "{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.SimpleInjector", "Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj", "{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{90559950-1B32-4119-A78E-517E2C71EE23}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -15,6 +19,14 @@ Global
|
|||||||
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8AB4FD33-4AAA-465C-8579-9DB3F3B23813}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D7EC6F86-EB3B-49C3-8FE7-6E8C1BB413A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{90559950-1B32-4119-A78E-517E2C71EE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -17,31 +17,26 @@ namespace Tapeti
|
|||||||
public string VirtualHost { get; set; } = "/";
|
public string VirtualHost { get; set; } = "/";
|
||||||
public string Username { get; set; } = "guest";
|
public string Username { get; set; } = "guest";
|
||||||
public string Password { get; set; } = "guest";
|
public string Password { get; set; } = "guest";
|
||||||
|
public string PublishExchange { get; set; } = "";
|
||||||
|
public string SubscribeExchange { get; set; } = "";
|
||||||
|
|
||||||
public IControllerFactory ControllerFactory
|
|
||||||
|
public IDependencyResolver DependencyResolver
|
||||||
{
|
{
|
||||||
get { return controllerFactory ?? (controllerFactory = new DefaultControllerFactory()); }
|
get { return dependencyResolver ?? (dependencyResolver = new DefaultDependencyResolver(GetPublisher)); }
|
||||||
set { controllerFactory = value; }
|
set { dependencyResolver = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IRoutingKeyStrategy RoutingKeyStrategy
|
private IDependencyResolver dependencyResolver;
|
||||||
{
|
private List<IQueueRegistration> registrations;
|
||||||
get { return routingKeyStrategy ?? (routingKeyStrategy = new DefaultRoutingKeyStrategy()); }
|
|
||||||
set { routingKeyStrategy = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private IControllerFactory controllerFactory;
|
|
||||||
private IRoutingKeyStrategy routingKeyStrategy;
|
|
||||||
private List<IMessageHandlerRegistration> registrations;
|
|
||||||
private TapetiWorker worker;
|
private TapetiWorker worker;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public TapetiConnection WithControllerFactory(IControllerFactory factory)
|
public TapetiConnection WithDependencyResolver(IDependencyResolver resolver)
|
||||||
{
|
{
|
||||||
controllerFactory = factory;
|
dependencyResolver = resolver;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,16 +52,22 @@ namespace Tapeti
|
|||||||
if (!string.IsNullOrEmpty(queueAttribute.Name))
|
if (!string.IsNullOrEmpty(queueAttribute.Name))
|
||||||
throw new ArgumentException("Dynamic queue attributes must not have a Name");
|
throw new ArgumentException("Dynamic queue attributes must not have a Name");
|
||||||
|
|
||||||
GetRegistrations().Add(new ControllerDynamicQueueRegistration(controllerFactory, routingKeyStrategy, type));
|
GetRegistrations().Add(new ControllerDynamicQueueRegistration(
|
||||||
|
DependencyResolver.Resolve<IControllerFactory>,
|
||||||
|
DependencyResolver.Resolve<IRoutingKeyStrategy>,
|
||||||
|
type, SubscribeExchange));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(queueAttribute.Name))
|
if (string.IsNullOrEmpty(queueAttribute.Name))
|
||||||
throw new ArgumentException("Non-dynamic queue attribute must have a Name");
|
throw new ArgumentException("Non-dynamic queue attribute must have a Name");
|
||||||
|
|
||||||
GetRegistrations().Add(new ControllerQueueRegistration(controllerFactory, routingKeyStrategy, type, queueAttribute.Name));
|
GetRegistrations().Add(new ControllerQueueRegistration(
|
||||||
|
DependencyResolver.Resolve<IControllerFactory>,
|
||||||
|
type, SubscribeExchange, queueAttribute.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(DependencyResolver as IDependencyInjector)?.RegisterController(type);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +87,15 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ISubscriber Subscribe()
|
public async Task<ISubscriber> Subscribe()
|
||||||
{
|
{
|
||||||
if (registrations == null || registrations.Count == 0)
|
if (registrations == null || registrations.Count == 0)
|
||||||
throw new ArgumentException("No controllers registered");
|
throw new ArgumentException("No controllers registered");
|
||||||
|
|
||||||
return new TapetiSubscriber(GetWorker(), registrations);
|
var subscriber = new TapetiSubscriber(GetWorker());
|
||||||
|
await subscriber.BindQueues(registrations);
|
||||||
|
|
||||||
|
return subscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -117,21 +121,24 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected List<IMessageHandlerRegistration> GetRegistrations()
|
protected List<IQueueRegistration> GetRegistrations()
|
||||||
{
|
{
|
||||||
return registrations ?? (registrations = new List<IMessageHandlerRegistration>());
|
return registrations ?? (registrations = new List<IQueueRegistration>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected TapetiWorker GetWorker()
|
protected TapetiWorker GetWorker()
|
||||||
{
|
{
|
||||||
return worker ?? (worker = new TapetiWorker
|
return worker ?? (worker = new TapetiWorker(
|
||||||
|
DependencyResolver.Resolve<IMessageSerializer>(),
|
||||||
|
DependencyResolver.Resolve<IRoutingKeyStrategy>())
|
||||||
{
|
{
|
||||||
HostName = HostName,
|
HostName = HostName,
|
||||||
Port = Port,
|
Port = Port,
|
||||||
VirtualHost = VirtualHost,
|
VirtualHost = VirtualHost,
|
||||||
Username = Username,
|
Username = Username,
|
||||||
Password = Password
|
Password = Password,
|
||||||
|
PublishExchange = PublishExchange
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
TapetiTypes.cs
Normal file
9
TapetiTypes.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
public enum ConsumeResponse
|
||||||
|
{
|
||||||
|
Ack,
|
||||||
|
Nack,
|
||||||
|
Requeue
|
||||||
|
}
|
||||||
|
}
|
129
Tasks/SingleThreadTaskQueue.cs
Normal file
129
Tasks/SingleThreadTaskQueue.cs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Tasks
|
||||||
|
{
|
||||||
|
public class SingleThreadTaskQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object previousTaskLock = new object();
|
||||||
|
private Task previousTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
private readonly Lazy<SingleThreadTaskScheduler> singleThreadScheduler = new Lazy<SingleThreadTaskScheduler>();
|
||||||
|
|
||||||
|
|
||||||
|
public Task Add(Action action)
|
||||||
|
{
|
||||||
|
lock (previousTaskLock)
|
||||||
|
{
|
||||||
|
previousTask = previousTask.ContinueWith(t => action(), CancellationToken.None
|
||||||
|
, TaskContinuationOptions.None
|
||||||
|
, singleThreadScheduler.Value);
|
||||||
|
|
||||||
|
return previousTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task<T> Add<T>(Func<T> func)
|
||||||
|
{
|
||||||
|
lock (previousTaskLock)
|
||||||
|
{
|
||||||
|
var task = previousTask.ContinueWith(t => func(), CancellationToken.None
|
||||||
|
, TaskContinuationOptions.None
|
||||||
|
, singleThreadScheduler.Value);
|
||||||
|
|
||||||
|
previousTask = task;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (singleThreadScheduler.IsValueCreated)
|
||||||
|
singleThreadScheduler.Value.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class SingleThreadTaskScheduler : TaskScheduler, IDisposable
|
||||||
|
{
|
||||||
|
public override int MaximumConcurrencyLevel => 1;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly Queue<Task> scheduledTasks = new Queue<Task>();
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
|
||||||
|
public SingleThreadTaskScheduler()
|
||||||
|
{
|
||||||
|
// ReSharper disable once ObjectCreationAsStatement - fire and forget!
|
||||||
|
new Thread(WorkerThread).Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (scheduledTasks)
|
||||||
|
{
|
||||||
|
disposed = true;
|
||||||
|
Monitor.PulseAll(scheduledTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected override void QueueTask(Task task)
|
||||||
|
{
|
||||||
|
if (disposed) return;
|
||||||
|
|
||||||
|
lock (scheduledTasks)
|
||||||
|
{
|
||||||
|
scheduledTasks.Enqueue(task);
|
||||||
|
Monitor.Pulse(scheduledTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected override IEnumerable<Task> GetScheduledTasks()
|
||||||
|
{
|
||||||
|
lock (scheduledTasks)
|
||||||
|
{
|
||||||
|
return scheduledTasks.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void WorkerThread()
|
||||||
|
{
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
Task task;
|
||||||
|
lock (scheduledTasks)
|
||||||
|
{
|
||||||
|
task = WaitAndDequeueTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
TryExecuteTask(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task WaitAndDequeueTask()
|
||||||
|
{
|
||||||
|
while (!scheduledTasks.Any() && !disposed)
|
||||||
|
Monitor.Wait(scheduledTasks);
|
||||||
|
|
||||||
|
return disposed ? null : scheduledTasks.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
Test/App.config
Normal file
6
Test/App.config
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
38
Test/Program.cs
Normal file
38
Test/Program.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using SimpleInjector;
|
||||||
|
using Tapeti;
|
||||||
|
using Tapeti.SimpleInjector;
|
||||||
|
|
||||||
|
namespace Test
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static void Main()
|
||||||
|
{
|
||||||
|
var container = new Container();
|
||||||
|
|
||||||
|
using (var connection = new TapetiConnection
|
||||||
|
{
|
||||||
|
PublishExchange = "test",
|
||||||
|
SubscribeExchange = "test"
|
||||||
|
}
|
||||||
|
.WithDependencyResolver(new SimpleInjectorDependencyResolver(container))
|
||||||
|
.RegisterAllControllers(typeof(Program).Assembly))
|
||||||
|
{
|
||||||
|
container.Register(() => connection.GetPublisher());
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Test/Properties/AssemblyInfo.cs
Normal file
36
Test/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("Test")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
||||||
|
[assembly: AssemblyProduct("Test")]
|
||||||
|
[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("90559950-1b32-4119-a78e-517e2c71ee23")]
|
||||||
|
|
||||||
|
// 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")]
|
77
Test/Test.csproj
Normal file
77
Test/Test.csproj
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?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>{90559950-1B32-4119-A78E-517E2C71EE23}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>Test</RootNamespace>
|
||||||
|
<AssemblyName>Test</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<TargetFrameworkProfile />
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<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' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="SimpleInjector, Version=3.2.7.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\SimpleInjector.3.2.7\lib\net45\SimpleInjector.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
|
<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="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="TestQueueController.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Tapeti.csproj">
|
||||||
|
<Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project>
|
||||||
|
<Name>Tapeti</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj">
|
||||||
|
<Project>{d7ec6f86-eb3b-49c3-8fe7-6e8c1bb413a6}</Project>
|
||||||
|
<Name>Tapeti.SimpleInjector</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</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>
|
44
Test/TestQueueController.cs
Normal file
44
Test/TestQueueController.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
4
Test/packages.config
Normal file
4
Test/packages.config
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="SimpleInjector" version="3.2.7" targetFramework="net461" />
|
||||||
|
</packages>
|
@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
|
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
|
||||||
<package id="RabbitMQ.Client" version="4.1.1" targetFramework="net461" />
|
<package id="RabbitMQ.Client" version="4.1.1" targetFramework="net461" />
|
||||||
</packages>
|
</packages>
|
Loading…
Reference in New Issue
Block a user