Work in progress for migrating from Saga to Flow
This commit is contained in:
parent
7bafd2f3c4
commit
a2970b6893
@ -1,16 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
public delegate object ValueFactory(IMessageContext context);
|
public delegate object ValueFactory(IMessageContext context);
|
||||||
|
public delegate Task ResultHandler(IMessageContext context, object value);
|
||||||
|
|
||||||
|
|
||||||
public interface IBindingContext
|
public interface IBindingContext
|
||||||
{
|
{
|
||||||
Type MessageClass { get; set; }
|
Type MessageClass { get; set; }
|
||||||
|
|
||||||
|
MethodInfo Method { get; }
|
||||||
IReadOnlyList<IBindingParameter> Parameters { get; }
|
IReadOnlyList<IBindingParameter> Parameters { get; }
|
||||||
|
IBindingResult Result { get; }
|
||||||
|
|
||||||
|
void Use(IBindingFilter filter);
|
||||||
|
void Use(IMessageMiddleware middleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -21,4 +29,13 @@ namespace Tapeti.Config
|
|||||||
|
|
||||||
void SetBinding(ValueFactory valueFactory);
|
void SetBinding(ValueFactory valueFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface IBindingResult
|
||||||
|
{
|
||||||
|
ParameterInfo Info { get; }
|
||||||
|
bool HasHandler { get; }
|
||||||
|
|
||||||
|
void SetHandler(ResultHandler resultHandler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
9
Config/IBindingFilter.cs
Normal file
9
Config/IBindingFilter.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Config
|
||||||
|
{
|
||||||
|
public interface IBindingFilter
|
||||||
|
{
|
||||||
|
Task<bool> Accept(IMessageContext context, IBinding binding);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ namespace Tapeti.Config
|
|||||||
IDependencyResolver DependencyResolver { get; }
|
IDependencyResolver DependencyResolver { get; }
|
||||||
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||||
IEnumerable<IQueue> Queues { get; }
|
IEnumerable<IQueue> Queues { get; }
|
||||||
|
|
||||||
|
IBinding GetBinding(Delegate method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -28,8 +30,17 @@ namespace Tapeti.Config
|
|||||||
Type Controller { get; }
|
Type Controller { get; }
|
||||||
MethodInfo Method { get; }
|
MethodInfo Method { get; }
|
||||||
Type MessageClass { get; }
|
Type MessageClass { get; }
|
||||||
|
string QueueName { get; }
|
||||||
|
|
||||||
|
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||||
|
|
||||||
bool Accept(object message);
|
bool Accept(object message);
|
||||||
Task<object> Invoke(IMessageContext context, object message);
|
Task<object> Invoke(IMessageContext context, object message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface IDynamicQueueBinding : IBinding
|
||||||
|
{
|
||||||
|
void SetQueueName(string queueName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
public interface IMessageContext
|
public interface IMessageContext : IDisposable
|
||||||
{
|
{
|
||||||
object Controller { get; }
|
IDependencyResolver DependencyResolver { get; }
|
||||||
|
|
||||||
|
string Queue { get; }
|
||||||
|
string RoutingKey { get; }
|
||||||
object Message { get; }
|
object Message { get; }
|
||||||
|
IBasicProperties Properties { get; }
|
||||||
|
|
||||||
IDictionary<string, object> Items { get; }
|
IDictionary<string, object> Items { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller will be null when passed to an IBindingFilter
|
||||||
|
/// </summary>
|
||||||
|
object Controller { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Config
|
namespace Tapeti.Config
|
||||||
{
|
{
|
||||||
public interface IMessageMiddleware
|
public interface IMessageMiddleware
|
||||||
{
|
{
|
||||||
void Handle(IMessageContext context, Action next);
|
Task Handle(IMessageContext context, Func<Task> next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,16 @@ namespace Tapeti.Connection
|
|||||||
public class TapetiConsumer : DefaultBasicConsumer
|
public class TapetiConsumer : DefaultBasicConsumer
|
||||||
{
|
{
|
||||||
private readonly TapetiWorker worker;
|
private readonly TapetiWorker worker;
|
||||||
|
private readonly string queueName;
|
||||||
private readonly IDependencyResolver dependencyResolver;
|
private readonly IDependencyResolver dependencyResolver;
|
||||||
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
private readonly IReadOnlyList<IMessageMiddleware> messageMiddleware;
|
||||||
private readonly List<IBinding> bindings;
|
private readonly List<IBinding> bindings;
|
||||||
|
|
||||||
|
|
||||||
public TapetiConsumer(TapetiWorker worker, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware)
|
public TapetiConsumer(TapetiWorker worker, string queueName, IDependencyResolver dependencyResolver, IEnumerable<IBinding> bindings, IReadOnlyList<IMessageMiddleware> messageMiddleware)
|
||||||
{
|
{
|
||||||
this.worker = worker;
|
this.worker = worker;
|
||||||
|
this.queueName = queueName;
|
||||||
this.dependencyResolver = dependencyResolver;
|
this.dependencyResolver = dependencyResolver;
|
||||||
this.messageMiddleware = messageMiddleware;
|
this.messageMiddleware = messageMiddleware;
|
||||||
this.bindings = bindings.ToList();
|
this.bindings = bindings.ToList();
|
||||||
@ -33,25 +35,41 @@ namespace Tapeti.Connection
|
|||||||
if (message == null)
|
if (message == null)
|
||||||
throw new ArgumentException("Empty message");
|
throw new ArgumentException("Empty message");
|
||||||
|
|
||||||
var handled = false;
|
var validMessageType = false;
|
||||||
foreach (var binding in bindings.Where(b => b.Accept(message)))
|
foreach (var binding in bindings.Where(b => b.Accept(message)))
|
||||||
{
|
{
|
||||||
var context = new MessageContext
|
using (var context = new MessageContext
|
||||||
{
|
{
|
||||||
|
DependencyResolver = dependencyResolver,
|
||||||
Controller = dependencyResolver.Resolve(binding.Controller),
|
Controller = dependencyResolver.Resolve(binding.Controller),
|
||||||
Message = message
|
Queue = queueName,
|
||||||
};
|
RoutingKey = routingKey,
|
||||||
|
Message = message,
|
||||||
|
Properties = properties
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// ReSharper disable AccessToDisposedClosure - MiddlewareHelper will not keep a reference to the lambdas
|
||||||
|
MiddlewareHelper.GoAsync(
|
||||||
|
binding.MessageMiddleware != null
|
||||||
|
? messageMiddleware.Concat(binding.MessageMiddleware).ToList()
|
||||||
|
: messageMiddleware,
|
||||||
|
async (handler, next) => await handler.Handle(context, next),
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var result = binding.Invoke(context, message).Result;
|
||||||
|
|
||||||
MiddlewareHelper.Go(messageMiddleware, (handler, next) => handler.Handle(context, next));
|
// TODO change to result handler
|
||||||
|
if (result != null)
|
||||||
|
await worker.Publish(result, null);
|
||||||
|
}
|
||||||
|
).Wait();
|
||||||
|
// ReSharper restore AccessToDisposedClosure
|
||||||
|
}
|
||||||
|
|
||||||
var result = binding.Invoke(context, message).Result;
|
validMessageType = true;
|
||||||
if (result != null)
|
|
||||||
worker.Publish(result);
|
|
||||||
|
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handled)
|
if (!validMessageType)
|
||||||
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
|
throw new ArgumentException($"Unsupported message type: {message.GetType().FullName}");
|
||||||
|
|
||||||
worker.Respond(deliveryTag, ConsumeResponse.Ack);
|
worker.Respond(deliveryTag, ConsumeResponse.Ack);
|
||||||
@ -66,9 +84,23 @@ namespace Tapeti.Connection
|
|||||||
|
|
||||||
protected class MessageContext : IMessageContext
|
protected class MessageContext : IMessageContext
|
||||||
{
|
{
|
||||||
|
public IDependencyResolver DependencyResolver { get; set; }
|
||||||
|
|
||||||
public object Controller { get; set; }
|
public object Controller { get; set; }
|
||||||
|
|
||||||
|
public string Queue { get; set; }
|
||||||
|
public string RoutingKey { get; set; }
|
||||||
public object Message { get; set; }
|
public object Message { get; set; }
|
||||||
|
public IBasicProperties Properties { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, object> Items { get; } = new Dictionary<string, object>();
|
public IDictionary<string, object> Items { get; } = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var value in Items.Values)
|
||||||
|
(value as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
namespace Tapeti.Connection
|
namespace Tapeti.Connection
|
||||||
{
|
{
|
||||||
public class TapetiPublisher : IPublisher
|
public class TapetiPublisher : IAdvancedPublisher
|
||||||
{
|
{
|
||||||
private readonly Func<TapetiWorker> workerFactory;
|
private readonly Func<TapetiWorker> workerFactory;
|
||||||
|
|
||||||
@ -16,7 +17,19 @@ namespace Tapeti.Connection
|
|||||||
|
|
||||||
public Task Publish(object message)
|
public Task Publish(object message)
|
||||||
{
|
{
|
||||||
return workerFactory().Publish(message);
|
return workerFactory().Publish(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task Publish(object message, IBasicProperties properties)
|
||||||
|
{
|
||||||
|
return workerFactory().Publish(message, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task PublishDirect(object message, string queueName, IBasicProperties properties)
|
||||||
|
{
|
||||||
|
return workerFactory().PublishDirect(message, queueName, properties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,17 +32,15 @@ namespace Tapeti.Connection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Publish(object message)
|
public Task Publish(object message, IBasicProperties properties)
|
||||||
{
|
{
|
||||||
return taskQueue.Value.Add(async () =>
|
return Publish(message, properties, Exchange, routingKeyStrategy.GetRoutingKey(message.GetType()));
|
||||||
{
|
}
|
||||||
var properties = new BasicProperties();
|
|
||||||
var body = messageSerializer.Serialize(message, properties);
|
|
||||||
|
|
||||||
(await GetChannel())
|
|
||||||
.BasicPublish(Exchange, routingKeyStrategy.GetRoutingKey(message.GetType()), false,
|
public Task PublishDirect(object message, string queueName, IBasicProperties properties)
|
||||||
properties, body);
|
{
|
||||||
}).Unwrap();
|
return Publish(message, properties, "", queueName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +48,7 @@ namespace Tapeti.Connection
|
|||||||
{
|
{
|
||||||
return taskQueue.Value.Add(async () =>
|
return taskQueue.Value.Add(async () =>
|
||||||
{
|
{
|
||||||
(await GetChannel()).BasicConsume(queueName, false, new TapetiConsumer(this, dependencyResolver, bindings, messageMiddleware));
|
(await GetChannel()).BasicConsume(queueName, false, new TapetiConsumer(this, queueName, dependencyResolver, bindings, messageMiddleware));
|
||||||
}).Unwrap();
|
}).Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +67,8 @@ namespace Tapeti.Connection
|
|||||||
{
|
{
|
||||||
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
|
var routingKey = routingKeyStrategy.GetRoutingKey(binding.MessageClass);
|
||||||
channel.QueueBind(dynamicQueue.QueueName, Exchange, routingKey);
|
channel.QueueBind(dynamicQueue.QueueName, Exchange, routingKey);
|
||||||
|
|
||||||
|
(binding as IDynamicQueueBinding)?.SetQueueName(dynamicQueue.QueueName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dynamicQueue.QueueName;
|
return dynamicQueue.QueueName;
|
||||||
@ -130,6 +130,22 @@ namespace Tapeti.Connection
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Task Publish(object message, IBasicProperties properties, string exchange, string routingKey)
|
||||||
|
{
|
||||||
|
return taskQueue.Value.Add(async () =>
|
||||||
|
{
|
||||||
|
var messageProperties = properties ?? new BasicProperties();
|
||||||
|
if (messageProperties.Timestamp.UnixTime == 0)
|
||||||
|
messageProperties.Timestamp = new AmqpTimestamp(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds());
|
||||||
|
|
||||||
|
var body = messageSerializer.Serialize(message, messageProperties);
|
||||||
|
|
||||||
|
(await GetChannel())
|
||||||
|
.BasicPublish(exchange, routingKey, false, messageProperties, body);
|
||||||
|
}).Unwrap();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Only call this from a task in the taskQueue to ensure IModel is only used
|
/// 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.
|
/// by a single thread, as is recommended in the RabbitMQ .NET Client documentation.
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.Default
|
|
||||||
{
|
|
||||||
// End of the line...
|
|
||||||
public class BindingBufferStop : IBindingMiddleware
|
|
||||||
{
|
|
||||||
public void Handle(IBindingContext context, Action next)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,21 +6,12 @@ namespace Tapeti.Default
|
|||||||
{
|
{
|
||||||
public class DependencyResolverBinding : IBindingMiddleware
|
public class DependencyResolverBinding : IBindingMiddleware
|
||||||
{
|
{
|
||||||
private readonly IDependencyResolver resolver;
|
|
||||||
|
|
||||||
|
|
||||||
public DependencyResolverBinding(IDependencyResolver resolver)
|
|
||||||
{
|
|
||||||
this.resolver = resolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Handle(IBindingContext context, Action next)
|
public void Handle(IBindingContext context, Action next)
|
||||||
{
|
{
|
||||||
next();
|
next();
|
||||||
|
|
||||||
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType.IsClass))
|
foreach (var parameter in context.Parameters.Where(p => !p.HasBinding && p.Info.ParameterType.IsClass))
|
||||||
parameter.SetBinding(messageContext => resolver.Resolve(parameter.Info.ParameterType));
|
parameter.SetBinding(messageContext => messageContext.DependencyResolver.Resolve(parameter.Info.ParameterType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ using RabbitMQ.Client;
|
|||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class DefaultMessageSerializer : IMessageSerializer
|
public class JsonMessageSerializer : IMessageSerializer
|
||||||
{
|
{
|
||||||
protected const string ContentType = "application/json";
|
protected const string ContentType = "application/json";
|
||||||
protected const string ClassTypeHeader = "classType";
|
protected const string ClassTypeHeader = "classType";
|
||||||
@ -18,7 +18,7 @@ namespace Tapeti.Default
|
|||||||
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
private readonly ConcurrentDictionary<Type, string> serializedTypeNames = new ConcurrentDictionary<Type, string>();
|
||||||
private readonly JsonSerializerSettings serializerSettings;
|
private readonly JsonSerializerSettings serializerSettings;
|
||||||
|
|
||||||
public DefaultMessageSerializer()
|
public JsonMessageSerializer()
|
||||||
{
|
{
|
||||||
serializerSettings = new JsonSerializerSettings
|
serializerSettings = new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
@ -47,8 +47,8 @@ namespace Tapeti.Default
|
|||||||
{
|
{
|
||||||
object typeName;
|
object typeName;
|
||||||
|
|
||||||
if (!properties.ContentType.Equals(ContentType))
|
if (properties.ContentType == null || !properties.ContentType.Equals(ContentType))
|
||||||
throw new ArgumentException("content_type must be {ContentType}");
|
throw new ArgumentException($"content_type must be {ContentType}");
|
||||||
|
|
||||||
if (properties.Headers == null || !properties.Headers.TryGetValue(ClassTypeHeader, out typeName))
|
if (properties.Headers == null || !properties.Headers.TryGetValue(ClassTypeHeader, out typeName))
|
||||||
throw new ArgumentException($"{ClassTypeHeader} header not present");
|
throw new ArgumentException($"{ClassTypeHeader} header not present");
|
31
Default/NamespaceMatchExchangeStrategy.cs
Normal file
31
Default/NamespaceMatchExchangeStrategy.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Tapeti.Default
|
||||||
|
{
|
||||||
|
public class NamespaceMatchExchangeStrategy : IExchangeStrategy
|
||||||
|
{
|
||||||
|
public const string DefaultFormat = "^Messaging\\.(.[^\\.]+)";
|
||||||
|
|
||||||
|
private readonly Regex namespaceRegEx;
|
||||||
|
|
||||||
|
|
||||||
|
public NamespaceMatchExchangeStrategy(string namespaceFormat = DefaultFormat)
|
||||||
|
{
|
||||||
|
namespaceRegEx = new Regex(namespaceFormat, RegexOptions.Compiled | RegexOptions.Singleline);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string GetExchange(Type messageType)
|
||||||
|
{
|
||||||
|
if (messageType.Namespace == null)
|
||||||
|
throw new ArgumentException($"{messageType.FullName} does not have a namespace");
|
||||||
|
|
||||||
|
var match = namespaceRegEx.Match(messageType.Namespace);
|
||||||
|
if (!match.Success)
|
||||||
|
throw new ArgumentException($"Namespace for {messageType.FullName} does not match the specified format");
|
||||||
|
|
||||||
|
return match.Groups[1].Value.ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace Tapeti.Default
|
namespace Tapeti.Default
|
||||||
{
|
{
|
||||||
public class DefaultRoutingKeyStrategy : IRoutingKeyStrategy
|
public class TypeNameRoutingKeyStrategy : IRoutingKeyStrategy
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<Type, string> routingKeyCache = new ConcurrentDictionary<Type, string>();
|
private readonly ConcurrentDictionary<Type, string> routingKeyCache = new ConcurrentDictionary<Type, string>();
|
||||||
|
|
@ -1,15 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Eventing.Reader;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Tapeti.Helpers
|
namespace Tapeti.Helpers
|
||||||
{
|
{
|
||||||
public static class MiddlewareHelper
|
public static class MiddlewareHelper
|
||||||
{
|
{
|
||||||
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle)
|
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler)
|
||||||
{
|
{
|
||||||
var handlerIndex = middleware.Count - 1;
|
var handlerIndex = middleware.Count - 1;
|
||||||
if (handlerIndex == -1)
|
if (handlerIndex == -1)
|
||||||
|
{
|
||||||
|
lastHandler();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Action handleNext = null;
|
Action handleNext = null;
|
||||||
|
|
||||||
@ -18,9 +23,35 @@ namespace Tapeti.Helpers
|
|||||||
handlerIndex--;
|
handlerIndex--;
|
||||||
if (handlerIndex >= 0)
|
if (handlerIndex >= 0)
|
||||||
handle(middleware[handlerIndex], handleNext);
|
handle(middleware[handlerIndex], handleNext);
|
||||||
|
else
|
||||||
|
lastHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
handle(middleware[handlerIndex], handleNext);
|
handle(middleware[handlerIndex], handleNext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<Task>, Task> handle, Func<Task> lastHandler)
|
||||||
|
{
|
||||||
|
var handlerIndex = middleware.Count - 1;
|
||||||
|
if (handlerIndex == -1)
|
||||||
|
{
|
||||||
|
await lastHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Func<Task> handleNext = null;
|
||||||
|
|
||||||
|
handleNext = async () =>
|
||||||
|
{
|
||||||
|
handlerIndex--;
|
||||||
|
if (handlerIndex >= 0)
|
||||||
|
await handle(middleware[handlerIndex], handleNext);
|
||||||
|
else
|
||||||
|
await lastHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
await handle(middleware[handlerIndex], handleNext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
@ -9,10 +10,11 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public interface IDependencyInjector : IDependencyResolver
|
public interface IDependencyContainer : IDependencyResolver
|
||||||
{
|
{
|
||||||
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
|
||||||
void RegisterPublisher(Func<IPublisher> publisher);
|
void RegisterPublisher(Func<IPublisher> publisher);
|
||||||
|
void RegisterConfig(IConfig config);
|
||||||
void RegisterController(Type type);
|
void RegisterController(Type type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
IExchangeStrategy.cs
Normal file
9
IExchangeStrategy.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti
|
||||||
|
{
|
||||||
|
public interface IExchangeStrategy
|
||||||
|
{
|
||||||
|
string GetExchange(Type messageType);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
|
||||||
namespace Tapeti
|
namespace Tapeti
|
||||||
{
|
{
|
||||||
@ -6,4 +7,11 @@ namespace Tapeti
|
|||||||
{
|
{
|
||||||
Task Publish(object message);
|
Task Publish(object message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface IAdvancedPublisher : IPublisher
|
||||||
|
{
|
||||||
|
Task Publish(object message, IBasicProperties properties);
|
||||||
|
Task PublishDirect(object message, string queueName, IBasicProperties properties);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
8
Tapeti.Flow/Annotations/ContinuationAttribute.cs
Normal file
8
Tapeti.Flow/Annotations/ContinuationAttribute.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Annotations
|
||||||
|
{
|
||||||
|
public class ContinuationAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
9
Tapeti.Flow/Annotations/RequestAttribute.cs
Normal file
9
Tapeti.Flow/Annotations/RequestAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Annotations
|
||||||
|
{
|
||||||
|
public class RequestAttribute : Attribute
|
||||||
|
{
|
||||||
|
public Type Response { get; set; }
|
||||||
|
}
|
||||||
|
}
|
7
Tapeti.Flow/ContextItems.cs
Normal file
7
Tapeti.Flow/ContextItems.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public static class ContextItems
|
||||||
|
{
|
||||||
|
public const string FlowContext = "Tapeti.Flow.FlowContext";
|
||||||
|
}
|
||||||
|
}
|
33
Tapeti.Flow/Default/DelegateYieldPoint.cs
Normal file
33
Tapeti.Flow/Default/DelegateYieldPoint.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
internal interface IExecutableYieldPoint : IYieldPoint
|
||||||
|
{
|
||||||
|
bool StoreState { get; }
|
||||||
|
|
||||||
|
Task Execute(FlowContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class DelegateYieldPoint : IYieldPoint
|
||||||
|
{
|
||||||
|
public bool StoreState { get; }
|
||||||
|
|
||||||
|
private readonly Func<FlowContext, Task> onExecute;
|
||||||
|
|
||||||
|
|
||||||
|
public DelegateYieldPoint(bool storeState, Func<FlowContext, Task> onExecute)
|
||||||
|
{
|
||||||
|
StoreState = storeState;
|
||||||
|
this.onExecute = onExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task Execute(FlowContext context)
|
||||||
|
{
|
||||||
|
return onExecute(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
Tapeti.Flow/Default/FlowBindingFilter.cs
Normal file
66
Tapeti.Flow/Default/FlowBindingFilter.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Flow.FlowHelpers;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class FlowBindingFilter : IBindingFilter
|
||||||
|
{
|
||||||
|
public async Task<bool> Accept(IMessageContext context, IBinding binding)
|
||||||
|
{
|
||||||
|
var flowContext = await GetFlowContext(context);
|
||||||
|
if (flowContext?.FlowState == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string continuation;
|
||||||
|
if (!flowContext.FlowState.Continuations.TryGetValue(flowContext.ContinuationID, out continuation))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return continuation == MethodSerializer.Serialize(binding.Method);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static async Task<FlowContext> GetFlowContext(IMessageContext context)
|
||||||
|
{
|
||||||
|
if (context.Items.ContainsKey(ContextItems.FlowContext))
|
||||||
|
return (FlowContext)context.Items[ContextItems.FlowContext];
|
||||||
|
|
||||||
|
if (context.Properties.CorrelationId == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Guid continuationID;
|
||||||
|
if (!Guid.TryParse(context.Properties.CorrelationId, out continuationID))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flowStore = context.DependencyResolver.Resolve<IFlowStore>();
|
||||||
|
|
||||||
|
var flowStateID = await flowStore.FindFlowStateID(continuationID);
|
||||||
|
if (!flowStateID.HasValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flowStateLock = await flowStore.LockFlowState(flowStateID.Value);
|
||||||
|
if (flowStateLock == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flowState = await flowStateLock.GetFlowState();
|
||||||
|
|
||||||
|
|
||||||
|
var flowMetadata = flowState != null ? Newtonsoft.Json.JsonConvert.DeserializeObject<FlowMetadata>(flowState.Metadata) : null;
|
||||||
|
//var continuationMetaData = Newtonsoft.Json.JsonConvert.DeserializeObject<ContinuationMetadata>(continuation.MetaData);
|
||||||
|
|
||||||
|
var flowContext = new FlowContext
|
||||||
|
{
|
||||||
|
MessageContext = context,
|
||||||
|
ContinuationID = continuationID,
|
||||||
|
FlowStateLock = flowStateLock,
|
||||||
|
FlowState = flowState,
|
||||||
|
Reply = flowMetadata?.Reply
|
||||||
|
};
|
||||||
|
|
||||||
|
// IDisposable items in the IMessageContext are automatically disposed
|
||||||
|
context.Items.Add(ContextItems.FlowContext, flowContext);
|
||||||
|
return flowContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
Tapeti.Flow/Default/FlowContext.cs
Normal file
44
Tapeti.Flow/Default/FlowContext.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
internal class FlowContext : IDisposable
|
||||||
|
{
|
||||||
|
public IMessageContext MessageContext { get; set; }
|
||||||
|
public IFlowStateLock FlowStateLock { get; set; }
|
||||||
|
public FlowState FlowState { get; set; }
|
||||||
|
public Guid ContinuationID { get; set; }
|
||||||
|
|
||||||
|
public FlowReplyMetadata Reply { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
MessageContext?.Dispose();
|
||||||
|
FlowStateLock?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class FlowReplyMetadata
|
||||||
|
{
|
||||||
|
public string ReplyTo { get; set; }
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public string ResponseTypeName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class FlowMetadata
|
||||||
|
{
|
||||||
|
public string ControllerTypeName { get; set; }
|
||||||
|
public FlowReplyMetadata Reply { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class ContinuationMetadata
|
||||||
|
{
|
||||||
|
public string MethodName { get; set; }
|
||||||
|
public string ConvergeMethodName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
25
Tapeti.Flow/Default/FlowMessageMiddleware.cs
Normal file
25
Tapeti.Flow/Default/FlowMessageMiddleware.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Flow.FlowHelpers;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class FlowMessageMiddleware : IMessageMiddleware
|
||||||
|
{
|
||||||
|
public async Task Handle(IMessageContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
var flowContext = (FlowContext)context.Items[ContextItems.FlowContext];
|
||||||
|
if (flowContext != null)
|
||||||
|
{
|
||||||
|
Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, context.Controller);
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
flowContext.FlowState.Continuations.Remove(flowContext.ContinuationID);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
262
Tapeti.Flow/Default/FlowProvider.cs
Normal file
262
Tapeti.Flow/Default/FlowProvider.cs
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RabbitMQ.Client.Framing;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Flow.Annotations;
|
||||||
|
using Tapeti.Flow.FlowHelpers;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class FlowProvider : IFlowProvider, IFlowHandler
|
||||||
|
{
|
||||||
|
private readonly IConfig config;
|
||||||
|
private readonly IAdvancedPublisher publisher;
|
||||||
|
|
||||||
|
|
||||||
|
public FlowProvider(IConfig config, IPublisher publisher)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.publisher = (IAdvancedPublisher)publisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler)
|
||||||
|
{
|
||||||
|
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
|
||||||
|
return new DelegateYieldPoint(true, context => SendRequest(context, message, responseHandlerInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler)
|
||||||
|
{
|
||||||
|
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
|
||||||
|
return new DelegateYieldPoint(true, context => SendRequest(context, message, responseHandlerInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IFlowParallelRequestBuilder YieldWithParallelRequest()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
//return new ParallelRequestBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IYieldPoint EndWithResponse<TResponse>(TResponse message)
|
||||||
|
{
|
||||||
|
return new DelegateYieldPoint(false, context => SendResponse(context, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IYieldPoint End()
|
||||||
|
{
|
||||||
|
return new DelegateYieldPoint(false, EndFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo)
|
||||||
|
{
|
||||||
|
var continuationID = Guid.NewGuid();
|
||||||
|
|
||||||
|
context.FlowState.Continuations.Add(continuationID,
|
||||||
|
Newtonsoft.Json.JsonConvert.SerializeObject(new ContinuationMetadata
|
||||||
|
{
|
||||||
|
MethodName = responseHandlerInfo.MethodName,
|
||||||
|
ConvergeMethodName = null
|
||||||
|
}));
|
||||||
|
|
||||||
|
var properties = new BasicProperties
|
||||||
|
{
|
||||||
|
CorrelationId = continuationID.ToString(),
|
||||||
|
ReplyTo = responseHandlerInfo.ReplyToQueue
|
||||||
|
};
|
||||||
|
|
||||||
|
await publisher.Publish(message, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task SendResponse(FlowContext context, object message)
|
||||||
|
{
|
||||||
|
if (context.Reply == null)
|
||||||
|
throw new InvalidOperationException("No response is required");
|
||||||
|
|
||||||
|
if (message.GetType().FullName != context.Reply.ResponseTypeName)
|
||||||
|
throw new InvalidOperationException($"Flow must end with a response message of type {context.Reply.ResponseTypeName}, {message.GetType().FullName} was returned instead");
|
||||||
|
|
||||||
|
var properties = new BasicProperties
|
||||||
|
{
|
||||||
|
CorrelationId = context.Reply.CorrelationId
|
||||||
|
};
|
||||||
|
|
||||||
|
await publisher.PublishDirect(message, context.Reply.ReplyTo, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Task EndFlow(FlowContext context)
|
||||||
|
{
|
||||||
|
if (context.Reply != null)
|
||||||
|
throw new InvalidOperationException($"Flow must end with a response message of type {context.Reply.ResponseTypeName}");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static ResponseHandlerInfo GetResponseHandlerInfo(IConfig config, object request, Delegate responseHandler)
|
||||||
|
{
|
||||||
|
var binding = config.GetBinding(responseHandler);
|
||||||
|
if (binding == null)
|
||||||
|
throw new ArgumentException("responseHandler must be a registered message handler", nameof(responseHandler));
|
||||||
|
|
||||||
|
var requestAttribute = request.GetType().GetCustomAttribute<RequestAttribute>();
|
||||||
|
if (requestAttribute?.Response != null && requestAttribute.Response != binding.MessageClass)
|
||||||
|
throw new ArgumentException($"responseHandler must accept message of type {binding.MessageClass}", nameof(responseHandler));
|
||||||
|
|
||||||
|
return new ResponseHandlerInfo
|
||||||
|
{
|
||||||
|
MethodName = MethodSerializer.Serialize(responseHandler.Method),
|
||||||
|
ReplyToQueue = binding.QueueName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Execute(IMessageContext context, IYieldPoint yieldPoint)
|
||||||
|
{
|
||||||
|
var flowContext = (FlowContext)context.Items[ContextItems.FlowContext];
|
||||||
|
if (flowContext == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var delegateYieldPoint = (DelegateYieldPoint)yieldPoint;
|
||||||
|
await delegateYieldPoint.Execute(flowContext);
|
||||||
|
|
||||||
|
if (delegateYieldPoint.StoreState)
|
||||||
|
{
|
||||||
|
flowContext.FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(context.Controller);
|
||||||
|
await flowContext.FlowStateLock.StoreFlowState(flowContext.FlowState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await flowContext.FlowStateLock.DeleteFlowState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
private class ParallelRequestBuilder : IFlowParallelRequestBuilder
|
||||||
|
{
|
||||||
|
internal class RequestInfo
|
||||||
|
{
|
||||||
|
public object Message { get; set; }
|
||||||
|
public ResponseHandlerInfo ResponseHandlerInfo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IConfig config;
|
||||||
|
private readonly IFlowStore flowStore;
|
||||||
|
private readonly Func<FlowContext, object, ResponseHandlerInfo, Task> sendRequest;
|
||||||
|
private readonly List<RequestInfo> requests = new List<RequestInfo>();
|
||||||
|
|
||||||
|
|
||||||
|
public ParallelRequestBuilder(IConfig config, IFlowStore flowStore, Func<FlowContext, object, ResponseHandlerInfo, Task> sendRequest)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.flowStore = flowStore;
|
||||||
|
this.sendRequest = sendRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler)
|
||||||
|
{
|
||||||
|
requests.Add(new RequestInfo
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
ResponseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler)
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler)
|
||||||
|
{
|
||||||
|
requests.Add(new RequestInfo
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
ResponseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler)
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IYieldPoint Yield(Func<Task<IYieldPoint>> continuation)
|
||||||
|
{
|
||||||
|
return new YieldPoint(flowStore, true, context => Task.WhenAll(requests.Select(requestInfo => sendRequest(context, requestInfo.Message, requestInfo.ResponseHandlerInfo))));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IYieldPoint Yield(Func<IYieldPoint> continuation)
|
||||||
|
{
|
||||||
|
return new YieldPoint(flowStore, true, context => Task.WhenAll(requests.Select(requestInfo => sendRequest(context, requestInfo.Message, requestInfo.ResponseHandlerInfo))));
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
internal class ResponseHandlerInfo
|
||||||
|
{
|
||||||
|
public string MethodName { get; set; }
|
||||||
|
public string ReplyToQueue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handle response (correlationId known)
|
||||||
|
internal async Task HandleMessage(object message, string correlationID)
|
||||||
|
{
|
||||||
|
var continuationID = Guid.Parse(correlationID);
|
||||||
|
var flowStateID = await owner.flowStore.FindFlowStateID(continuationID);
|
||||||
|
|
||||||
|
if (!flowStateID.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (flowStateLock = await owner.flowStore.LockFlowState(flowStateID.Value))
|
||||||
|
{
|
||||||
|
flowState = await flowStateLock.GetFlowState();
|
||||||
|
|
||||||
|
continuation = flowState.Continuations[continuationID];
|
||||||
|
if (continuation != null)
|
||||||
|
await HandleContinuation(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleContinuation(object message)
|
||||||
|
{
|
||||||
|
var flowMetaData = Newtonsoft.Json.JsonConvert.DeserializeObject<FlowMetaData>(flowState.MetaData);
|
||||||
|
var continuationMetaData =
|
||||||
|
Newtonsoft.Json.JsonConvert.DeserializeObject<ContinuationMetaData>(continuation.MetaData);
|
||||||
|
|
||||||
|
reply = flowMetaData.Reply;
|
||||||
|
controllerType = owner.GetControllerType(flowMetaData.ControllerTypeName);
|
||||||
|
method = controllerType.GetMethod(continuationMetaData.MethodName);
|
||||||
|
|
||||||
|
controller = owner.container.GetInstance(controllerType);
|
||||||
|
|
||||||
|
Newtonsoft.Json.JsonConvert.PopulateObject(flowState.Data, controller);
|
||||||
|
|
||||||
|
var yieldPoint = (AbstractYieldPoint) await owner.CallFlowController(controller, method, message);
|
||||||
|
|
||||||
|
await yieldPoint.Execute(this);
|
||||||
|
|
||||||
|
if (yieldPoint.Store)
|
||||||
|
{
|
||||||
|
flowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(controller);
|
||||||
|
flowState.Continuations.Remove(continuation);
|
||||||
|
|
||||||
|
await flowStateLock.StoreFlowState(flowState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await flowStateLock.DeleteFlowState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
186
Tapeti.Flow/Default/FlowStore.cs
Normal file
186
Tapeti.Flow/Default/FlowStore.cs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public class FlowStore : IFlowStore
|
||||||
|
{
|
||||||
|
private readonly IFlowRepository repository;
|
||||||
|
private readonly ConcurrentDictionary<Guid, FlowState> flowStates = new ConcurrentDictionary<Guid, FlowState>();
|
||||||
|
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new ConcurrentDictionary<Guid, Guid>();
|
||||||
|
|
||||||
|
|
||||||
|
public FlowStore(IFlowRepository repository)
|
||||||
|
{
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Load()
|
||||||
|
{
|
||||||
|
flowStates.Clear();
|
||||||
|
continuationLookup.Clear();
|
||||||
|
|
||||||
|
foreach (var state in await repository.GetAllStates())
|
||||||
|
{
|
||||||
|
flowStates.GetOrAdd(state.FlowID, new FlowState
|
||||||
|
{
|
||||||
|
Metadata = state.Metadata,
|
||||||
|
Data = state.Data,
|
||||||
|
Continuations = state.Continuations
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var continuation in state.Continuations)
|
||||||
|
continuationLookup.GetOrAdd(continuation.Key, state.FlowID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task<Guid?> FindFlowStateID(Guid continuationID)
|
||||||
|
{
|
||||||
|
Guid result;
|
||||||
|
return Task.FromResult(continuationLookup.TryGetValue(continuationID, out result) ? result : (Guid?)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IFlowStateLock> LockFlowState(Guid flowStateID)
|
||||||
|
{
|
||||||
|
var isNew = false;
|
||||||
|
var flowState = flowStates.GetOrAdd(flowStateID, id =>
|
||||||
|
{
|
||||||
|
isNew = true;
|
||||||
|
return new FlowState();
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = new FlowStateLock(this, flowState, flowStateID, isNew);
|
||||||
|
await result.Lock();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class FlowStateLock : IFlowStateLock
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
private readonly FlowStore owner;
|
||||||
|
private readonly FlowState flowState;
|
||||||
|
private readonly Guid flowID;
|
||||||
|
private bool isNew;
|
||||||
|
private bool isDisposed;
|
||||||
|
|
||||||
|
|
||||||
|
public FlowStateLock(FlowStore owner, FlowState flowState, Guid flowID, bool isNew)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
this.flowState = flowState;
|
||||||
|
this.flowID = flowID;
|
||||||
|
this.isNew = isNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task Lock()
|
||||||
|
{
|
||||||
|
return semaphore.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (flowState)
|
||||||
|
{
|
||||||
|
if (!isDisposed)
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid FlowStateID => flowID;
|
||||||
|
|
||||||
|
public Task<FlowState> GetFlowState()
|
||||||
|
{
|
||||||
|
lock (flowState)
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
throw new ObjectDisposedException("FlowStateLock");
|
||||||
|
|
||||||
|
return Task.FromResult(new FlowState
|
||||||
|
{
|
||||||
|
Data = flowState.Data,
|
||||||
|
Metadata = flowState.Metadata,
|
||||||
|
Continuations = flowState.Continuations.ToDictionary(kv => kv.Key, kv => kv.Value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StoreFlowState(FlowState newFlowState)
|
||||||
|
{
|
||||||
|
lock (flowState)
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
throw new ObjectDisposedException("FlowStateLock");
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var removedContinuation in
|
||||||
|
flowState.Continuations.Keys.Where(
|
||||||
|
k => !newFlowState.Continuations.ContainsKey(k)))
|
||||||
|
{
|
||||||
|
Guid removedValue;
|
||||||
|
owner.continuationLookup.TryRemove(removedContinuation, out removedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var addedContinuation in
|
||||||
|
newFlowState.Continuations.Where(
|
||||||
|
c => !flowState.Continuations.ContainsKey(c.Key)))
|
||||||
|
{
|
||||||
|
owner.continuationLookup.TryAdd(addedContinuation.Key, flowID);
|
||||||
|
}
|
||||||
|
|
||||||
|
flowState.Metadata = newFlowState.Metadata;
|
||||||
|
flowState.Data = newFlowState.Data;
|
||||||
|
flowState.Continuations = newFlowState.Continuations.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||||
|
}
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
isNew = false;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
await
|
||||||
|
owner.repository.CreateState(flowID, now, flowState.Metadata, flowState.Data, flowState.Continuations);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await owner.repository.UpdateState(flowID, flowState.Metadata, flowState.Data, flowState.Continuations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteFlowState()
|
||||||
|
{
|
||||||
|
lock (flowState)
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
throw new ObjectDisposedException("FlowStateLock");
|
||||||
|
|
||||||
|
foreach (var removedContinuation in flowState.Continuations.Keys)
|
||||||
|
{
|
||||||
|
Guid removedValue;
|
||||||
|
owner.continuationLookup.TryRemove(removedContinuation, out removedValue);
|
||||||
|
}
|
||||||
|
FlowState removedFlow;
|
||||||
|
owner.flowStates.TryRemove(flowID, out removedFlow);
|
||||||
|
}
|
||||||
|
if (!isNew)
|
||||||
|
await owner.repository.DeleteState(flowID);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
Tapeti.Flow/Default/NonPersistentFlowRepository.cs
Normal file
30
Tapeti.Flow/Default/NonPersistentFlowRepository.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
public class NonPersistentFlowRepository : IFlowRepository
|
||||||
|
{
|
||||||
|
public Task<IQueryable<FlowStateRecord>> GetAllStates()
|
||||||
|
{
|
||||||
|
return Task.FromResult(new List<FlowStateRecord>().AsQueryable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateState(Guid flowID, DateTime timestamp, string metadata, string data, IDictionary<Guid, string> continuations)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateState(Guid flowID, string metadata, string data, IDictionary<Guid, string> continuations)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteState(Guid flowID)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
Tapeti.Flow/FlowHelpers/MethodSerializer.cs
Normal file
12
Tapeti.Flow/FlowHelpers/MethodSerializer.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.FlowHelpers
|
||||||
|
{
|
||||||
|
public static class MethodSerializer
|
||||||
|
{
|
||||||
|
public static string Serialize(MethodInfo method)
|
||||||
|
{
|
||||||
|
return method.Name + '@' + method.DeclaringType?.Assembly.GetName().Name + ':' + method.DeclaringType?.FullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
Tapeti.Flow/FlowMiddleware.cs
Normal file
77
Tapeti.Flow/FlowMiddleware.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
using Tapeti.Flow.Annotations;
|
||||||
|
using Tapeti.Flow.Default;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public class FlowMiddleware : IMiddlewareBundle
|
||||||
|
{
|
||||||
|
public IEnumerable<object> GetContents(IDependencyResolver dependencyResolver)
|
||||||
|
{
|
||||||
|
var container = dependencyResolver as IDependencyContainer;
|
||||||
|
if (container != null)
|
||||||
|
{
|
||||||
|
container.RegisterDefault<IFlowProvider, FlowProvider>();
|
||||||
|
container.RegisterDefault<IFlowHandler, FlowProvider>();
|
||||||
|
// TODO singleton
|
||||||
|
container.RegisterDefault<IFlowStore, FlowStore>();
|
||||||
|
container.RegisterDefault<IFlowRepository, NonPersistentFlowRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new[] { new FlowBindingMiddleware() };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class FlowBindingMiddleware : IBindingMiddleware
|
||||||
|
{
|
||||||
|
public void Handle(IBindingContext context, Action next)
|
||||||
|
{
|
||||||
|
HandleContinuationFilter(context);
|
||||||
|
HandleYieldPointResult(context);
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void HandleContinuationFilter(IBindingContext context)
|
||||||
|
{
|
||||||
|
var continuationAttribute = context.Method.GetCustomAttribute<ContinuationAttribute>();
|
||||||
|
if (continuationAttribute != null)
|
||||||
|
{
|
||||||
|
context.Use(new FlowBindingFilter());
|
||||||
|
context.Use(new FlowMessageMiddleware());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void HandleYieldPointResult(IBindingContext context)
|
||||||
|
{
|
||||||
|
if (context.Result.Info.ParameterType == typeof(IYieldPoint))
|
||||||
|
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value));
|
||||||
|
|
||||||
|
else if (context.Result.Info.ParameterType == typeof(Task<>))
|
||||||
|
{
|
||||||
|
var genericArguments = context.Result.Info.ParameterType.GetGenericArguments();
|
||||||
|
if (genericArguments.Length == 1 && genericArguments[0] == typeof(IYieldPoint))
|
||||||
|
context.Result.SetHandler(async (messageContext, value) =>
|
||||||
|
{
|
||||||
|
var yieldPoint = await (Task<IYieldPoint>)value;
|
||||||
|
if (yieldPoint != null)
|
||||||
|
await HandleYieldPoint(messageContext, yieldPoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Task HandleYieldPoint(IMessageContext context, IYieldPoint yieldPoint)
|
||||||
|
{
|
||||||
|
var flowHandler = context.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
|
return flowHandler.Execute(context, yieldPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
Tapeti.Flow/IFlowProvider.cs
Normal file
39
Tapeti.Flow/IFlowProvider.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public interface IFlowProvider
|
||||||
|
{
|
||||||
|
IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler);
|
||||||
|
|
||||||
|
// One does not simply overload methods with Task vs non-Task Funcs. "Ambiguous call".
|
||||||
|
// Apparantly this is because a return type of a method is not part of its signature,
|
||||||
|
// according to: http://stackoverflow.com/questions/18715979/ambiguity-with-action-and-func-parameter
|
||||||
|
IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler);
|
||||||
|
|
||||||
|
IFlowParallelRequestBuilder YieldWithParallelRequest();
|
||||||
|
|
||||||
|
IYieldPoint EndWithResponse<TResponse>(TResponse message);
|
||||||
|
IYieldPoint End();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFlowHandler
|
||||||
|
{
|
||||||
|
Task Execute(IMessageContext context, IYieldPoint yieldPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFlowParallelRequestBuilder
|
||||||
|
{
|
||||||
|
IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler);
|
||||||
|
IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler);
|
||||||
|
|
||||||
|
IYieldPoint Yield(Func<Task<IYieldPoint>> continuation);
|
||||||
|
IYieldPoint Yield(Func<IYieldPoint> continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IYieldPoint
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
24
Tapeti.Flow/IFlowRepository.cs
Normal file
24
Tapeti.Flow/IFlowRepository.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public interface IFlowRepository
|
||||||
|
{
|
||||||
|
Task<IQueryable<FlowStateRecord>> GetAllStates();
|
||||||
|
Task CreateState(Guid flowID, DateTime timestamp, string metadata, string data, IDictionary<Guid, string> continuations);
|
||||||
|
Task UpdateState(Guid flowID, string metadata, string data, IDictionary<Guid, string> continuations);
|
||||||
|
Task DeleteState(Guid flowID);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FlowStateRecord
|
||||||
|
{
|
||||||
|
public Guid FlowID;
|
||||||
|
public string Metadata;
|
||||||
|
public string Data;
|
||||||
|
public Dictionary<Guid, string> Continuations;
|
||||||
|
}
|
||||||
|
}
|
28
Tapeti.Flow/IFlowStore.cs
Normal file
28
Tapeti.Flow/IFlowStore.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
public interface IFlowStore
|
||||||
|
{
|
||||||
|
Task Load();
|
||||||
|
Task<Guid?> FindFlowStateID(Guid continuationID);
|
||||||
|
Task<IFlowStateLock> LockFlowState(Guid flowStateID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFlowStateLock : IDisposable
|
||||||
|
{
|
||||||
|
Guid FlowStateID { get; }
|
||||||
|
Task<FlowState> GetFlowState();
|
||||||
|
Task StoreFlowState(FlowState flowState);
|
||||||
|
Task DeleteFlowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FlowState
|
||||||
|
{
|
||||||
|
public string Metadata { get; set; }
|
||||||
|
public string Data { get; set; }
|
||||||
|
public Dictionary<Guid, string> Continuations { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,12 @@ using System.Runtime.InteropServices;
|
|||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
// set of attributes. Change these attribute values to modify the information
|
// set of attributes. Change these attribute values to modify the information
|
||||||
// associated with an assembly.
|
// associated with an assembly.
|
||||||
[assembly: AssemblyTitle("Tapeti.Saga")]
|
[assembly: AssemblyTitle("Tapeti.Flow")]
|
||||||
[assembly: AssemblyDescription("")]
|
[assembly: AssemblyDescription("")]
|
||||||
[assembly: AssemblyConfiguration("")]
|
[assembly: AssemblyConfiguration("")]
|
||||||
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
[assembly: AssemblyCompany("")]
|
||||||
[assembly: AssemblyProduct("Tapeti.Saga")]
|
[assembly: AssemblyProduct("Tapeti.Flow")]
|
||||||
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard Company 2016")]
|
[assembly: AssemblyCopyright("")]
|
||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
@ -7,8 +7,8 @@
|
|||||||
<ProjectGuid>{F84AD920-D5A1-455D-AED5-2542B3A47B85}</ProjectGuid>
|
<ProjectGuid>{F84AD920-D5A1-455D-AED5-2542B3A47B85}</ProjectGuid>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
<RootNamespace>Tapeti.Saga</RootNamespace>
|
<RootNamespace>Tapeti.Flow</RootNamespace>
|
||||||
<AssemblyName>Tapeti.Saga</AssemblyName>
|
<AssemblyName>Tapeti.Flow</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<TargetFrameworkProfile />
|
<TargetFrameworkProfile />
|
||||||
@ -31,6 +31,14 @@
|
|||||||
<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">
|
||||||
|
<HintPath>..\packages\RabbitMQ.Client.4.1.1\lib\net451\RabbitMQ.Client.dll</HintPath>
|
||||||
|
<Private>True</Private>
|
||||||
|
</Reference>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
@ -41,15 +49,22 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="ISaga.cs" />
|
<Compile Include="Annotations\ContinuationAttribute.cs" />
|
||||||
<Compile Include="ISagaProvider.cs" />
|
<Compile Include="Annotations\RequestAttribute.cs" />
|
||||||
<Compile Include="ISagaStore.cs" />
|
<Compile Include="ContextItems.cs" />
|
||||||
|
<Compile Include="Default\FlowBindingFilter.cs" />
|
||||||
|
<Compile Include="Default\FlowContext.cs" />
|
||||||
|
<Compile Include="Default\FlowMessageMiddleware.cs" />
|
||||||
|
<Compile Include="Default\NonPersistentFlowRepository.cs" />
|
||||||
|
<Compile Include="Default\DelegateYieldPoint.cs" />
|
||||||
|
<Compile Include="FlowHelpers\MethodSerializer.cs" />
|
||||||
|
<Compile Include="FlowMiddleware.cs" />
|
||||||
|
<Compile Include="Default\FlowStore.cs" />
|
||||||
|
<Compile Include="Default\FlowProvider.cs" />
|
||||||
|
<Compile Include="IFlowRepository.cs" />
|
||||||
|
<Compile Include="IFlowStore.cs" />
|
||||||
|
<Compile Include="IFlowProvider.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="SagaBindingMiddleware.cs" />
|
|
||||||
<Compile Include="SagaMemoryStore.cs" />
|
|
||||||
<Compile Include="SagaMessageMiddleware.cs" />
|
|
||||||
<Compile Include="SagaMiddleware.cs" />
|
|
||||||
<Compile Include="SagaProvider.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tapeti.csproj">
|
<ProjectReference Include="..\Tapeti.csproj">
|
||||||
@ -57,6 +72,9 @@
|
|||||||
<Name>Tapeti</Name>
|
<Name>Tapeti</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- 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.
|
Other similar extension points exist, see Microsoft.Common.targets.
|
5
Tapeti.Flow/packages.config
Normal file
5
Tapeti.Flow/packages.config
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
|
||||||
|
<package id="RabbitMQ.Client" version="4.1.1" targetFramework="net461" />
|
||||||
|
</packages>
|
@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Tapeti.Saga
|
|
||||||
{
|
|
||||||
public interface ISaga<out T> : IDisposable where T : class
|
|
||||||
{
|
|
||||||
string Id { get; }
|
|
||||||
T State { get; }
|
|
||||||
|
|
||||||
void ExpectResponse(string callId);
|
|
||||||
void ResolveResponse(string callId);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Saga
|
|
||||||
{
|
|
||||||
public interface ISagaProvider
|
|
||||||
{
|
|
||||||
Task<ISaga<T>> Begin<T>(T initialState) where T : class;
|
|
||||||
Task<ISaga<T>> Continue<T>(string sagaId) where T : class;
|
|
||||||
Task<object> Continue(string sagaId);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Tapeti.Saga
|
|
||||||
{
|
|
||||||
public interface ISagaStore
|
|
||||||
{
|
|
||||||
Task<object> Read(string sagaId);
|
|
||||||
Task Update(string sagaId, object state);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
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,10 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using SimpleInjector;
|
using SimpleInjector;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
namespace Tapeti.SimpleInjector
|
namespace Tapeti.SimpleInjector
|
||||||
{
|
{
|
||||||
public class SimpleInjectorDependencyResolver : IDependencyInjector
|
public class SimpleInjectorDependencyResolver : IDependencyContainer
|
||||||
{
|
{
|
||||||
private readonly Container container;
|
private readonly Container container;
|
||||||
|
|
||||||
@ -32,6 +33,12 @@ namespace Tapeti.SimpleInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void RegisterConfig(IConfig config)
|
||||||
|
{
|
||||||
|
container.RegisterSingleton(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void RegisterPublisher(Func<IPublisher> publisher)
|
public void RegisterPublisher(Func<IPublisher> publisher)
|
||||||
{
|
{
|
||||||
// ReSharper disable once SimplifyLinqExpression - still not a fan of negative predicates
|
// ReSharper disable once SimplifyLinqExpression - still not a fan of negative predicates
|
||||||
|
@ -53,18 +53,22 @@
|
|||||||
<Compile Include="Annotations\MessageControllerAttribute.cs" />
|
<Compile Include="Annotations\MessageControllerAttribute.cs" />
|
||||||
<Compile Include="Annotations\StaticQueueAttribute.cs" />
|
<Compile Include="Annotations\StaticQueueAttribute.cs" />
|
||||||
<Compile Include="Annotations\DynamicQueueAttribute.cs" />
|
<Compile Include="Annotations\DynamicQueueAttribute.cs" />
|
||||||
|
<Compile Include="Config\IBindingFilter.cs" />
|
||||||
<Compile Include="Connection\TapetiConsumer.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="Default\ConsoleLogger.cs" />
|
<Compile Include="Default\ConsoleLogger.cs" />
|
||||||
<Compile Include="Default\DevNullLogger.cs" />
|
<Compile Include="Default\DevNullLogger.cs" />
|
||||||
|
<Compile Include="Default\JsonMessageSerializer.cs" />
|
||||||
|
<Compile Include="Default\NamespaceMatchExchangeStrategy.cs" />
|
||||||
|
<Compile Include="Default\TypeNameRoutingKeyStrategy.cs" />
|
||||||
<Compile Include="Helpers\ConsoleHelper.cs" />
|
<Compile Include="Helpers\ConsoleHelper.cs" />
|
||||||
<Compile Include="Helpers\MiddlewareHelper.cs" />
|
<Compile Include="Helpers\MiddlewareHelper.cs" />
|
||||||
<Compile Include="IConnection.cs" />
|
<Compile Include="IConnection.cs" />
|
||||||
|
<Compile Include="IExchangeStrategy.cs" />
|
||||||
<Compile Include="ILogger.cs" />
|
<Compile Include="ILogger.cs" />
|
||||||
<Compile Include="Config\IMessageContext.cs" />
|
<Compile Include="Config\IMessageContext.cs" />
|
||||||
<Compile Include="Default\BindingBufferStop.cs" />
|
|
||||||
<Compile Include="Config\IMessageMiddleware.cs" />
|
<Compile Include="Config\IMessageMiddleware.cs" />
|
||||||
<Compile Include="Config\IMiddlewareBundle.cs" />
|
<Compile Include="Config\IMiddlewareBundle.cs" />
|
||||||
<Compile Include="Config\IConfig.cs" />
|
<Compile Include="Config\IConfig.cs" />
|
||||||
@ -74,8 +78,6 @@
|
|||||||
<Compile Include="TapetiConfig.cs" />
|
<Compile Include="TapetiConfig.cs" />
|
||||||
<Compile Include="TapetiTypes.cs" />
|
<Compile Include="TapetiTypes.cs" />
|
||||||
<Compile Include="Tasks\SingleThreadTaskQueue.cs" />
|
<Compile Include="Tasks\SingleThreadTaskQueue.cs" />
|
||||||
<Compile Include="Default\DefaultMessageSerializer.cs" />
|
|
||||||
<Compile Include="Default\DefaultRoutingKeyStrategy.cs" />
|
|
||||||
<Compile Include="IDependencyResolver.cs" />
|
<Compile Include="IDependencyResolver.cs" />
|
||||||
<Compile Include="IMessageSerializer.cs" />
|
<Compile Include="IMessageSerializer.cs" />
|
||||||
<Compile Include="IPublisher.cs" />
|
<Compile Include="IPublisher.cs" />
|
||||||
|
@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.SimpleInjector", "Ta
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{90559950-1B32-4119-A78E-517E2C71EE23}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{90559950-1B32-4119-A78E-517E2C71EE23}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Saga", "Tapeti.Saga\Tapeti.Saga.csproj", "{F84AD920-D5A1-455D-AED5-2542B3A47B85}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tapeti.Flow", "Tapeti.Flow\Tapeti.Flow.csproj", "{F84AD920-D5A1-455D-AED5-2542B3A47B85}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
5
Tapeti.sln.DotSettings.user
Normal file
5
Tapeti.sln.DotSettings.user
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/@KeyIndexDefined">True</s:Boolean>
|
||||||
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/Coords/@EntryValue">(Doc Ln 29 Col 51)</s:String>
|
||||||
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/FileId/@EntryValue">F84AD920-D5A1-455D-AED5-2542B3A47B85/d:Default/f:FlowProvider.cs</s:String>
|
||||||
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark9/Owner/@EntryValue">NumberedBookmarkManager</s:String></wpf:ResourceDictionary>
|
149
TapetiConfig.cs
149
TapetiConfig.cs
@ -36,25 +36,14 @@ namespace Tapeti
|
|||||||
this.exchange = exchange;
|
this.exchange = exchange;
|
||||||
this.dependencyResolver = dependencyResolver;
|
this.dependencyResolver = dependencyResolver;
|
||||||
|
|
||||||
Use(new BindingBufferStop());
|
Use(new DependencyResolverBinding());
|
||||||
Use(new DependencyResolverBinding(dependencyResolver));
|
|
||||||
Use(new MessageBinding());
|
Use(new MessageBinding());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public IConfig Build()
|
public IConfig Build()
|
||||||
{
|
{
|
||||||
var dependencyInjector = dependencyResolver as IDependencyInjector;
|
RegisterDefaults();
|
||||||
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>();
|
var queues = new List<IQueue>();
|
||||||
queues.AddRange(staticRegistrations.Select(qb => new Queue(new QueueInfo { Dynamic = false, Name = qb.Key }, qb.Value)));
|
queues.AddRange(staticRegistrations.Select(qb => new Queue(new QueueInfo { Dynamic = false, Name = qb.Key }, qb.Value)));
|
||||||
@ -73,7 +62,10 @@ namespace Tapeti
|
|||||||
|
|
||||||
queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true }, bl)));
|
queues.AddRange(dynamicBindings.Select(bl => new Queue(new QueueInfo { Dynamic = true }, bl)));
|
||||||
|
|
||||||
return new Config(exchange, dependencyResolver, messageMiddleware, queues);
|
var config = new Config(exchange, dependencyResolver, messageMiddleware, queues);
|
||||||
|
(dependencyResolver as IDependencyContainer)?.RegisterConfig(config);
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -108,6 +100,23 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void RegisterDefaults()
|
||||||
|
{
|
||||||
|
var container = dependencyResolver as IDependencyContainer;
|
||||||
|
if (container != null)
|
||||||
|
{
|
||||||
|
if (ConsoleHelper.IsAvailable())
|
||||||
|
container.RegisterDefault<ILogger, ConsoleLogger>();
|
||||||
|
else
|
||||||
|
container.RegisterDefault<ILogger, DevNullLogger>();
|
||||||
|
|
||||||
|
container.RegisterDefault<IMessageSerializer, JsonMessageSerializer>();
|
||||||
|
container.RegisterDefault<IExchangeStrategy, NamespaceMatchExchangeStrategy>();
|
||||||
|
container.RegisterDefault<IRoutingKeyStrategy, TypeNameRoutingKeyStrategy>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public TapetiConfig RegisterController(Type controller)
|
public TapetiConfig RegisterController(Type controller)
|
||||||
{
|
{
|
||||||
var controllerQueueInfo = GetQueueInfo(controller);
|
var controllerQueueInfo = GetQueueInfo(controller);
|
||||||
@ -120,7 +129,7 @@ namespace Tapeti
|
|||||||
if (!methodQueueInfo.IsValid)
|
if (!methodQueueInfo.IsValid)
|
||||||
throw new TopologyConfigurationException($"Method {method.Name} or controller {controller.Name} requires a queue attribute");
|
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 context = new BindingContext(method);
|
||||||
var messageHandler = GetMessageHandler(context, method);
|
var messageHandler = GetMessageHandler(context, method);
|
||||||
|
|
||||||
var handlerInfo = new Binding
|
var handlerInfo = new Binding
|
||||||
@ -129,7 +138,8 @@ namespace Tapeti
|
|||||||
Method = method,
|
Method = method,
|
||||||
QueueInfo = methodQueueInfo,
|
QueueInfo = methodQueueInfo,
|
||||||
MessageClass = context.MessageClass,
|
MessageClass = context.MessageClass,
|
||||||
MessageHandler = messageHandler
|
MessageHandler = messageHandler,
|
||||||
|
MessageMiddleware = context.MessageMiddleware
|
||||||
};
|
};
|
||||||
|
|
||||||
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
if (methodQueueInfo.Dynamic.GetValueOrDefault())
|
||||||
@ -159,7 +169,7 @@ namespace Tapeti
|
|||||||
|
|
||||||
protected MessageHandlerFunc GetMessageHandler(IBindingContext context, MethodInfo method)
|
protected MessageHandlerFunc GetMessageHandler(IBindingContext context, MethodInfo method)
|
||||||
{
|
{
|
||||||
MiddlewareHelper.Go(bindingMiddleware, (handler, next) => handler.Handle(context, next));
|
MiddlewareHelper.Go(bindingMiddleware, (handler, next) => handler.Handle(context, next), () => {});
|
||||||
|
|
||||||
if (context.MessageClass == null)
|
if (context.MessageClass == null)
|
||||||
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} does not resolve to a message class");
|
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} does not resolve to a message class");
|
||||||
@ -250,6 +260,8 @@ namespace Tapeti
|
|||||||
var existing = staticRegistrations[binding.QueueInfo.Name];
|
var existing = staticRegistrations[binding.QueueInfo.Name];
|
||||||
|
|
||||||
// Technically we could easily do multicasting, but it complicates exception handling and requeueing
|
// Technically we could easily do multicasting, but it complicates exception handling and requeueing
|
||||||
|
// TODO allow multiple, if there is a filter which guarantees uniqueness
|
||||||
|
// TODO move to independant validation middleware
|
||||||
if (existing.Any(h => h.MessageClass == binding.MessageClass))
|
if (existing.Any(h => h.MessageClass == binding.MessageClass))
|
||||||
throw new TopologyConfigurationException($"Multiple handlers for message class {binding.MessageClass.Name} in queue {binding.QueueInfo.Name}");
|
throw new TopologyConfigurationException($"Multiple handlers for message class {binding.MessageClass.Name} in queue {binding.QueueInfo.Name}");
|
||||||
|
|
||||||
@ -303,13 +315,24 @@ namespace Tapeti
|
|||||||
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
|
||||||
public IEnumerable<IQueue> Queues { get; }
|
public IEnumerable<IQueue> Queues { get; }
|
||||||
|
|
||||||
|
private readonly Dictionary<MethodInfo, IBinding> bindingMethodLookup;
|
||||||
|
|
||||||
|
|
||||||
public Config(string exchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
|
public Config(string exchange, IDependencyResolver dependencyResolver, IReadOnlyList<IMessageMiddleware> messageMiddleware, IEnumerable<IQueue> queues)
|
||||||
{
|
{
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
DependencyResolver = dependencyResolver;
|
DependencyResolver = dependencyResolver;
|
||||||
MessageMiddleware = messageMiddleware;
|
MessageMiddleware = messageMiddleware;
|
||||||
Queues = queues;
|
Queues = queues.ToList();
|
||||||
|
|
||||||
|
bindingMethodLookup = Queues.SelectMany(q => q.Bindings).ToDictionary(b => b.Method, b => b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IBinding GetBinding(Delegate method)
|
||||||
|
{
|
||||||
|
IBinding binding;
|
||||||
|
return bindingMethodLookup.TryGetValue(method.Method, out binding) ? binding : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,16 +353,35 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected class Binding : IBinding
|
protected class Binding : IDynamicQueueBinding
|
||||||
{
|
{
|
||||||
public Type Controller { get; set; }
|
public Type Controller { get; set; }
|
||||||
public MethodInfo Method { get; set; }
|
public MethodInfo Method { get; set; }
|
||||||
public Type MessageClass { get; set; }
|
public Type MessageClass { get; set; }
|
||||||
|
public string QueueName { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
|
||||||
|
|
||||||
|
private QueueInfo queueInfo;
|
||||||
|
public QueueInfo QueueInfo
|
||||||
|
{
|
||||||
|
get { return queueInfo; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
QueueName = (value?.Dynamic).GetValueOrDefault() ? value?.Name : null;
|
||||||
|
queueInfo = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public QueueInfo QueueInfo { get; set; }
|
|
||||||
public MessageHandlerFunc MessageHandler { get; set; }
|
public MessageHandlerFunc MessageHandler { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void SetQueueName(string queueName)
|
||||||
|
{
|
||||||
|
QueueName = queueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool Accept(object message)
|
public bool Accept(object message)
|
||||||
{
|
{
|
||||||
return message.GetType() == MessageClass;
|
return message.GetType() == MessageClass;
|
||||||
@ -359,15 +401,52 @@ namespace Tapeti
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
internal interface IBindingResultAccess
|
||||||
|
{
|
||||||
|
ResultHandler GetHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
internal class BindingContext : IBindingContext
|
internal class BindingContext : IBindingContext
|
||||||
{
|
{
|
||||||
|
private List<IMessageMiddleware> messageMiddleware;
|
||||||
|
private List<IBindingFilter> bindingFilters;
|
||||||
|
|
||||||
public Type MessageClass { get; set; }
|
public Type MessageClass { get; set; }
|
||||||
|
|
||||||
|
public MethodInfo Method { get; }
|
||||||
public IReadOnlyList<IBindingParameter> Parameters { get; }
|
public IReadOnlyList<IBindingParameter> Parameters { get; }
|
||||||
|
public IBindingResult Result { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<IMessageMiddleware> MessageMiddleware => messageMiddleware;
|
||||||
|
public IReadOnlyList<IBindingFilter> BindingFilters => bindingFilters;
|
||||||
|
|
||||||
|
|
||||||
public BindingContext(IReadOnlyList<IBindingParameter> parameters)
|
public BindingContext(MethodInfo method)
|
||||||
{
|
{
|
||||||
Parameters = parameters;
|
Method = method;
|
||||||
|
|
||||||
|
Parameters = method.GetParameters().Select(p => new BindingParameter(p)).ToList();
|
||||||
|
Result = new BindingResult(method.ReturnParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Use(IMessageMiddleware middleware)
|
||||||
|
{
|
||||||
|
if (messageMiddleware == null)
|
||||||
|
messageMiddleware = new List<IMessageMiddleware>();
|
||||||
|
|
||||||
|
messageMiddleware.Add(middleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Use(IBindingFilter filter)
|
||||||
|
{
|
||||||
|
if (bindingFilters == null)
|
||||||
|
bindingFilters = new List<IBindingFilter>();
|
||||||
|
|
||||||
|
bindingFilters.Add(filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,5 +475,31 @@ namespace Tapeti
|
|||||||
binding = valueFactory;
|
binding = valueFactory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class BindingResult : IBindingResult, IBindingResultAccess
|
||||||
|
{
|
||||||
|
private ResultHandler handler;
|
||||||
|
|
||||||
|
public ParameterInfo Info { get; }
|
||||||
|
public bool HasHandler => handler != null;
|
||||||
|
|
||||||
|
|
||||||
|
public BindingResult(ParameterInfo parameter)
|
||||||
|
{
|
||||||
|
Info = parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ResultHandler GetHandler()
|
||||||
|
{
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHandler(ResultHandler resultHandler)
|
||||||
|
{
|
||||||
|
handler = resultHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace Tapeti
|
|||||||
public TapetiConnection(IConfig config)
|
public TapetiConnection(IConfig config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
(config.DependencyResolver as IDependencyInjector)?.RegisterPublisher(GetPublisher);
|
(config.DependencyResolver as IDependencyContainer)?.RegisterPublisher(GetPublisher);
|
||||||
|
|
||||||
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config.DependencyResolver, config.MessageMiddleware)
|
worker = new Lazy<TapetiWorker>(() => new TapetiWorker(config.DependencyResolver, config.MessageMiddleware)
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.SqlServer.Server;
|
using System.Threading.Tasks;
|
||||||
using Tapeti;
|
using Tapeti;
|
||||||
using Tapeti.Annotations;
|
using Tapeti.Annotations;
|
||||||
|
using Tapeti.Flow;
|
||||||
|
using Tapeti.Flow.Annotations;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
{
|
{
|
||||||
@ -9,62 +11,64 @@ namespace Test
|
|||||||
public class MarcoController : MessageController
|
public class MarcoController : MessageController
|
||||||
{
|
{
|
||||||
private readonly IPublisher publisher;
|
private readonly IPublisher publisher;
|
||||||
|
private readonly IFlowProvider flowProvider;
|
||||||
|
private readonly Visualizer visualizer;
|
||||||
|
|
||||||
|
// Public properties are automatically stored and retrieved while in a flow
|
||||||
|
public Guid StateTestGuid;
|
||||||
|
|
||||||
|
|
||||||
public MarcoController(IPublisher publisher/*, ISagaProvider sagaProvider*/)
|
public MarcoController(IPublisher publisher, IFlowProvider flowProvider, Visualizer visualizer)
|
||||||
{
|
{
|
||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
|
this.flowProvider = flowProvider;
|
||||||
|
this.visualizer = visualizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//[StaticQueue("test")]
|
/**
|
||||||
public PoloMessage Marco(MarcoMessage message, Visualizer visualizer)
|
* The Visualizer could've been injected through the constructor, which is
|
||||||
|
* the recommended way. Just testing the injection middleware here.
|
||||||
|
*/
|
||||||
|
public async Task Marco(MarcoMessage message, Visualizer myVisualizer)
|
||||||
{
|
{
|
||||||
visualizer.VisualizeMarco();
|
await myVisualizer.VisualizeMarco();
|
||||||
|
await publisher.Publish(new PoloMessage());
|
||||||
/*
|
|
||||||
using (sagaProvider.Begin<MarcoState>(new MarcoState
|
|
||||||
{
|
|
||||||
...
|
|
||||||
}))
|
|
||||||
{
|
|
||||||
//publisher.Publish(new PoloColorRequest(), saga, PoloColorResponse1);
|
|
||||||
//publisher.Publish(new PoloColorRequest(), saga, callID = "tweede");
|
|
||||||
|
|
||||||
// Saga refcount = 2
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return new PoloMessage(); ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
public IYieldPoint Polo(PoloMessage message)
|
||||||
[CallID("eerste")]
|
|
||||||
Implicit:
|
|
||||||
|
|
||||||
using (sagaProvider.Continue(correlatieID))
|
|
||||||
{
|
|
||||||
saga refcount--;
|
|
||||||
public void PoloColorResponse1(PoloColorResponse message, ISaga<MarcoState> saga)
|
|
||||||
{
|
{
|
||||||
|
StateTestGuid = Guid.NewGuid();
|
||||||
|
|
||||||
saga.State == MarcoState
|
return flowProvider.YieldWithRequest<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(
|
||||||
|
new PoloConfirmationRequestMessage()
|
||||||
|
{
|
||||||
|
StoredInState = StateTestGuid
|
||||||
state.Color = message.Color;
|
},
|
||||||
|
HandlePoloConfirmationResponse);
|
||||||
if (state.Complete)
|
|
||||||
{
|
|
||||||
publisher.Publish(new PoloMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
public void Polo(PoloMessage message, Visualizer visualizer)
|
|
||||||
|
public async Task<IYieldPoint> HandlePoloConfirmationResponse(PoloConfirmationResponseMessage message)
|
||||||
{
|
{
|
||||||
visualizer.VisualizePolo();
|
await visualizer.VisualizePolo(message.ShouldMatchState.Equals(StateTestGuid));
|
||||||
|
return flowProvider.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For simple request response patterns, the return type can be used.
|
||||||
|
* This will automatically include the correlationId in the response and
|
||||||
|
* use the replyTo header of the request if provided.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public PoloConfirmationResponseMessage PoloConfirmation(PoloConfirmationRequestMessage message)
|
||||||
|
{
|
||||||
|
return new PoloConfirmationResponseMessage
|
||||||
|
{
|
||||||
|
ShouldMatchState = message.StoredInState
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,15 +83,15 @@ namespace Test
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Request(Response = typeof(PoloConfirmationResponseMessage))]
|
||||||
|
public class PoloConfirmationRequestMessage
|
||||||
public class PoloColorRequest
|
|
||||||
{
|
{
|
||||||
|
public Guid StoredInState { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PoloColorResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
|
public class PoloConfirmationResponseMessage
|
||||||
|
{
|
||||||
|
public Guid ShouldMatchState { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using SimpleInjector;
|
using SimpleInjector;
|
||||||
using Tapeti;
|
using Tapeti;
|
||||||
using Tapeti.Saga;
|
using Tapeti.Flow;
|
||||||
using Tapeti.SimpleInjector;
|
using Tapeti.SimpleInjector;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
@ -13,10 +13,10 @@ namespace Test
|
|||||||
var container = new Container();
|
var container = new Container();
|
||||||
container.Register<MarcoEmitter>();
|
container.Register<MarcoEmitter>();
|
||||||
container.Register<Visualizer>();
|
container.Register<Visualizer>();
|
||||||
container.Register<ISagaStore, SagaMemoryStore>();
|
//container.RegisterSingleton<ISagaStore, SagaMemoryStore>();
|
||||||
|
|
||||||
var config = new TapetiConfig("test", new SimpleInjectorDependencyResolver(container))
|
var config = new TapetiConfig("test", new SimpleInjectorDependencyResolver(container))
|
||||||
.Use(new SagaMiddleware())
|
.Use(new FlowMiddleware())
|
||||||
.RegisterAllControllers()
|
.RegisterAllControllers()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
@ -63,9 +63,9 @@
|
|||||||
<Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project>
|
<Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project>
|
||||||
<Name>Tapeti</Name>
|
<Name>Tapeti</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\Tapeti.Saga\Tapeti.Saga.csproj">
|
<ProjectReference Include="..\Tapeti.Flow\Tapeti.Flow.csproj">
|
||||||
<Project>{f84ad920-d5a1-455d-aed5-2542b3a47b85}</Project>
|
<Project>{f84ad920-d5a1-455d-aed5-2542b3a47b85}</Project>
|
||||||
<Name>Tapeti.Saga</Name>
|
<Name>Tapeti.Flow</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj">
|
<ProjectReference Include="..\Tapeti.SimpleInjector\Tapeti.SimpleInjector.csproj">
|
||||||
<Project>{d7ec6f86-eb3b-49c3-8fe7-6e8c1bb413a6}</Project>
|
<Project>{d7ec6f86-eb3b-49c3-8fe7-6e8c1bb413a6}</Project>
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Test
|
namespace Test
|
||||||
{
|
{
|
||||||
public class Visualizer
|
public class Visualizer
|
||||||
{
|
{
|
||||||
public void VisualizeMarco()
|
public Task VisualizeMarco()
|
||||||
{
|
{
|
||||||
Console.WriteLine("Marco!");
|
Console.WriteLine("Marco!");
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void VisualizePolo()
|
public Task VisualizePolo(bool matches)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Polo!");
|
Console.WriteLine(matches ? "Polo!" : "Oops! Mismatch!");
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user