1
0
mirror of synced 2024-06-29 07:17:39 +00:00

Work in progress for migrating from Saga to Flow

This commit is contained in:
Mark van Renswoude 2017-01-31 12:01:08 +01:00
parent 7bafd2f3c4
commit a2970b6893
53 changed files with 1325 additions and 389 deletions

View File

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

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Tapeti.Config
{
public interface IBindingFilter
{
Task<bool> Accept(IMessageContext context, IBinding binding);
}
}

View File

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

View File

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

View File

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

View File

@ -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,
MiddlewareHelper.Go(messageMiddleware, (handler, next) => handler.Handle(context, next)); 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; var result = binding.Invoke(context, message).Result;
if (result != null)
worker.Publish(result);
handled = true; // TODO change to result handler
if (result != null)
await worker.Publish(result, null);
}
).Wait();
// ReSharper restore AccessToDisposedClosure
} }
if (!handled) validMessageType = true;
}
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();
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

@ -0,0 +1,9 @@
using System;
namespace Tapeti
{
public interface IExchangeStrategy
{
string GetExchange(Type messageType);
}
}

View File

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

View File

@ -0,0 +1,8 @@
using System;
namespace Tapeti.Flow.Annotations
{
public class ContinuationAttribute : Attribute
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Tapeti.Flow.Annotations
{
public class RequestAttribute : Attribute
{
public Type Response { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Tapeti.Flow
{
public static class ContextItems
{
public const string FlowContext = "Tapeti.Flow.FlowContext";
}
}

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

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

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

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

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

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

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

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

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

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

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

View File

@ -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("")]

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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
} }
public IYieldPoint Polo(PoloMessage message)
{
StateTestGuid = Guid.NewGuid();
return flowProvider.YieldWithRequest<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(
new PoloConfirmationRequestMessage()
{
StoredInState = StateTestGuid
},
HandlePoloConfirmationResponse);
}
public async Task<IYieldPoint> HandlePoloConfirmationResponse(PoloConfirmationResponseMessage message)
{
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.
*/ */
return new PoloMessage(); ; public PoloConfirmationResponseMessage PoloConfirmation(PoloConfirmationRequestMessage message)
}
/*
[CallID("eerste")]
Implicit:
using (sagaProvider.Continue(correlatieID))
{ {
saga refcount--; return new PoloConfirmationResponseMessage
public void PoloColorResponse1(PoloColorResponse message, ISaga<MarcoState> saga)
{ {
ShouldMatchState = message.StoredInState
saga.State == MarcoState };
state.Color = message.Color;
if (state.Complete)
{
publisher.Publish(new PoloMessage());
}
}
*/
public void Polo(PoloMessage message, Visualizer visualizer)
{
visualizer.VisualizePolo();
} }
} }
@ -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; }
} }
} }

View File

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

View File

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

View File

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