1
0
mirror of synced 2024-11-25 04:03:09 +01:00

Added AppSettings ConnectionParams helper

Changed BindingFilters to MessageFilterMiddleware (in preparation for SignalR interaction package)
Start of SqlConnectionFlowRepository
This commit is contained in:
Mark van Renswoude 2017-02-08 15:52:24 +01:00
parent 1f41f6bcc0
commit 6779f3a4d0
20 changed files with 252 additions and 83 deletions

View File

@ -5,9 +5,9 @@ namespace Tapeti.Flow.SQL
{ {
public static class ConfigExtensions public static class ConfigExtensions
{ {
public static TapetiConfig WithFlowSqlRepository(this TapetiConfig config) public static TapetiConfig WithFlowSqlRepository(this TapetiConfig config, string connectionString, int serviceId, string schema = "dbo")
{ {
config.Use(new FlowSqlRepositoryBundle()); config.Use(new FlowSqlRepositoryBundle(connectionString, serviceId, schema));
return config; return config;
} }
} }
@ -15,19 +15,25 @@ namespace Tapeti.Flow.SQL
internal class FlowSqlRepositoryBundle : ITapetiExtension internal class FlowSqlRepositoryBundle : ITapetiExtension
{ {
/* private readonly string connectionString;
public IEnumerable<object> GetContents(IDependencyResolver dependencyResolver) private readonly string schema;
{ private readonly int serviceId;
((IDependencyContainer)dependencyResolver)?.RegisterDefault<IFlowRepository, >();
return null;
public FlowSqlRepositoryBundle(string connectionString, int serviceId, string schema)
{
this.connectionString = connectionString;
this.serviceId = serviceId;
this.schema = schema;
} }
*/
public void RegisterDefaults(IDependencyContainer container) public void RegisterDefaults(IDependencyContainer container)
{ {
container.RegisterDefault<IFlowRepository>(() => new SqlConnectionFlowRepository(connectionString, serviceId, schema));
} }
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver) public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{ {
return null; return null;

View File

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
namespace Tapeti.Flow.SQL
{
/*
Assumes the following table layout (schema configurable):
create table dbo.Flow
(
FlowID uniqueidentifier not null,
ServiceID int not null,
CreationTime datetime2(3) not null,
Metadata nvarchar(max) null,
Flowdata nvarchar(max) null,
constraint PK_Flow primary key clustered (FlowID)
);
create table dbo.FlowContinuation
(
FlowID uniqueidentifier not null,
ContinuationID uniqueidentifier not null,
Metadata nvarchar(max) null,
constraint PK_FlowContinuation primary key clustered (FlowID, ContinuationID)
);
go;
alter table shared.FlowContinuation with check add constraint FK_FlowContinuation_Flow foreign key (FlowID) references shared.Flow (FlowID);
*/
public class SqlConnectionFlowRepository : IFlowRepository
{
private readonly string connectionString;
private readonly int serviceId;
private readonly string schema;
public SqlConnectionFlowRepository(string connectionString, int serviceId, string schema)
{
this.connectionString = connectionString;
this.serviceId = serviceId;
this.schema = schema;
}
public async Task<IQueryable<FlowStateRecord>> GetStates()
{
var result = new List<FlowStateRecord>();
using (var connection = await GetConnection())
{
var flowQuery = new SqlCommand($"select FlowID, Metadata, Flowdata from {schema}.Flow " +
"where ServiceID = @ServiceID " +
"order by FlowID", connection);
var flowServiceParam = flowQuery.Parameters.Add("@ServiceID", SqlDbType.Int);
var continuationQuery = new SqlCommand($"select FlowID, ContinuationID, Metadata from {schema}.FlowContinuation " +
"where ServiceID = @ServiceID " +
"order by FlowID", connection);
var continuationQueryParam = flowQuery.Parameters.Add("@ServiceID", SqlDbType.Int);
flowServiceParam.Value = serviceId;
continuationQueryParam.Value = serviceId;
var flowReader = await flowQuery.ExecuteReaderAsync();
var continuationReader = await continuationQuery.ExecuteReaderAsync();
var hasContinuation = await continuationReader.ReadAsync();
while (await flowReader.ReadAsync())
{
var flowStateRecord = new FlowStateRecord
{
FlowID = flowReader.GetGuid(0),
Metadata = flowReader.GetString(1),
Data = flowReader.GetString(2),
ContinuationMetadata = new Dictionary<Guid, string>()
};
while (hasContinuation && continuationReader.GetGuid(0) == flowStateRecord.FlowID)
{
flowStateRecord.ContinuationMetadata.Add(
continuationReader.GetGuid(1),
continuationReader.GetString(2)
);
hasContinuation = await continuationReader.ReadAsync();
}
result.Add(flowStateRecord);
}
}
return result.AsQueryable();
}
public Task CreateState(FlowStateRecord stateRecord, DateTime timestamp)
{
throw new NotImplementedException();
}
public Task UpdateState(FlowStateRecord stateRecord)
{
throw new NotImplementedException();
}
public Task DeleteState(Guid flowID)
{
throw new NotImplementedException();
}
private async Task<SqlConnection> GetConnection()
{
var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
return connection;
}
}
}

View File

@ -43,6 +43,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="ConfigExtensions.cs" /> <Compile Include="ConfigExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SqlConnectionFlowRepository.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Tapeti.Flow\Tapeti.Flow.csproj"> <ProjectReference Include="..\Tapeti.Flow\Tapeti.Flow.csproj">

View File

@ -27,7 +27,7 @@ namespace Tapeti.Flow.Default
if (continuationAttribute == null) if (continuationAttribute == null)
return; return;
context.Use(new FlowBindingFilter()); context.Use(new FlowMessageFilterMiddleware());
context.Use(new FlowMessageMiddleware()); context.Use(new FlowMessageMiddleware());
} }

View File

@ -5,15 +5,18 @@ using Tapeti.Flow.FlowHelpers;
namespace Tapeti.Flow.Default namespace Tapeti.Flow.Default
{ {
public class FlowBindingFilter : IBindingFilter public class FlowMessageFilterMiddleware : IMessageFilterMiddleware
{ {
public async Task<bool> Accept(IMessageContext context, IBinding binding) public async Task Handle(IMessageContext context, Func<Task> next)
{ {
var flowContext = await GetFlowContext(context); var flowContext = await GetFlowContext(context);
if (flowContext?.ContinuationMetadata == null) if (flowContext?.ContinuationMetadata == null)
return false; return;
return flowContext.ContinuationMetadata.MethodName == MethodSerializer.Serialize(binding.Method); if (flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(context.Binding.Method))
return;
await next();
} }

View File

@ -52,7 +52,7 @@
<Compile Include="Annotations\ContinuationAttribute.cs" /> <Compile Include="Annotations\ContinuationAttribute.cs" />
<Compile Include="Annotations\RequestAttribute.cs" /> <Compile Include="Annotations\RequestAttribute.cs" />
<Compile Include="ContextItems.cs" /> <Compile Include="ContextItems.cs" />
<Compile Include="Default\FlowBindingFilter.cs" /> <Compile Include="Default\FlowMessageFilterMiddleware.cs" />
<Compile Include="Default\FlowBindingMiddleware.cs" /> <Compile Include="Default\FlowBindingMiddleware.cs" />
<Compile Include="Default\FlowContext.cs" /> <Compile Include="Default\FlowContext.cs" />
<Compile Include="Default\FlowMessageMiddleware.cs" /> <Compile Include="Default\FlowMessageMiddleware.cs" />

View File

@ -17,7 +17,7 @@ namespace Tapeti.Config
IReadOnlyList<IBindingParameter> Parameters { get; } IReadOnlyList<IBindingParameter> Parameters { get; }
IBindingResult Result { get; } IBindingResult Result { get; }
void Use(IBindingFilter filter); void Use(IMessageFilterMiddleware filterMiddleware);
void Use(IMessageMiddleware middleware); void Use(IMessageMiddleware middleware);
} }

View File

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

View File

@ -31,10 +31,10 @@ namespace Tapeti.Config
Type MessageClass { get; } Type MessageClass { get; }
string QueueName { get; } string QueueName { get; }
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; } IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
IReadOnlyList<IBindingFilter> BindingFilters { get; }
Task<bool> Accept(IMessageContext context, object message); bool Accept(IMessageContext context, object message);
Task Invoke(IMessageContext context, object message); Task Invoke(IMessageContext context, object message);
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using RabbitMQ.Client; using RabbitMQ.Client;
namespace Tapeti.Config namespace Tapeti.Config
@ -17,13 +16,10 @@ namespace Tapeti.Config
IDictionary<string, object> Items { get; } IDictionary<string, object> Items { get; }
/// <summary> /// <summary>
/// Controller will be null when passed to an IBindingFilter /// Controller will be null when passed to a IMessageFilterMiddleware
/// </summary> /// </summary>
object Controller { get; } object Controller { get; }
/// <summary>
/// Binding will be null when passed to an IBindingFilter
/// </summary>
IBinding Binding { get; } IBinding Binding { get; }
} }
} }

View File

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Tapeti.Config
{
public interface IMessageFilterMiddleware
{
Task Handle(IMessageContext context, Func<Task> next);
}
}

View File

@ -53,20 +53,27 @@ namespace Tapeti.Connection
{ {
foreach (var binding in bindings) foreach (var binding in bindings)
{ {
if (!binding.Accept(context, message).Result) if (!binding.Accept(context, message))
continue; continue;
context.Controller = dependencyResolver.Resolve(binding.Controller);
context.Binding = binding; context.Binding = binding;
// ReSharper disable AccessToDisposedClosure - MiddlewareHelper will not keep a reference to the lambdas // ReSharper disable AccessToDisposedClosure - MiddlewareHelper will not keep a reference to the lambdas
MiddlewareHelper.GoAsync( MiddlewareHelper.GoAsync(
binding.MessageFilterMiddleware,
async (handler, next) => await handler.Handle(context, next),
async () =>
{
context.Controller = dependencyResolver.Resolve(binding.Controller);
await MiddlewareHelper.GoAsync(
binding.MessageMiddleware != null binding.MessageMiddleware != null
? messageMiddleware.Concat(binding.MessageMiddleware).ToList() ? messageMiddleware.Concat(binding.MessageMiddleware).ToList()
: messageMiddleware, : messageMiddleware,
async (handler, next) => await handler.Handle(context, next), async (handler, next) => await handler.Handle(context, next),
() => binding.Invoke(context, message) () => binding.Invoke(context, message)
).Wait(); );
}).Wait();
// ReSharper restore AccessToDisposedClosure // ReSharper restore AccessToDisposedClosure
validMessageType = true; validMessageType = true;

View File

@ -8,8 +8,8 @@ namespace Tapeti.Helpers
{ {
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler) public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler)
{ {
var handlerIndex = middleware.Count - 1; var handlerIndex = middleware?.Count - 1 ?? -1;
if (handlerIndex == -1) if (middleware == null || handlerIndex == -1)
{ {
lastHandler(); lastHandler();
return; return;
@ -32,8 +32,8 @@ namespace Tapeti.Helpers
public static async Task GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<Task>, Task> handle, Func<Task> lastHandler) public static async Task GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<Task>, Task> handle, Func<Task> lastHandler)
{ {
var handlerIndex = middleware.Count - 1; var handlerIndex = middleware?.Count - 1 ?? -1;
if (handlerIndex == -1) if (middleware == null || handlerIndex == -1)
{ {
await lastHandler(); await lastHandler();
return; return;

View File

@ -40,6 +40,7 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@ -53,7 +54,7 @@
<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="Config\IMessageFilterMiddleware.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" />
@ -78,6 +79,7 @@
<Compile Include="Config\IConfig.cs" /> <Compile Include="Config\IConfig.cs" />
<Compile Include="MessageController.cs" /> <Compile Include="MessageController.cs" />
<Compile Include="Config\IBindingMiddleware.cs" /> <Compile Include="Config\IBindingMiddleware.cs" />
<Compile Include="TapetiAppSettingsConnectionParams.cs" />
<Compile Include="TapetiConnectionParams.cs" /> <Compile Include="TapetiConnectionParams.cs" />
<Compile Include="TapetiConfig.cs" /> <Compile Include="TapetiConfig.cs" />
<Compile Include="ConsumeResponse.cs" /> <Compile Include="ConsumeResponse.cs" />

View File

@ -0,0 +1,36 @@
using System;
using System.Configuration;
using System.Linq;
namespace Tapeti
{
public class TapetiAppSettingsConnectionParams : TapetiConnectionParams
{
public const string DefaultPrefix = "rabbitmq:";
public const string KeyHostname = "hostname";
public const string KeyPort = "port";
public const string KeyVirtualHost = "virtualhost";
public const string KeyUsername = "username";
public const string KeyPassword = "password";
public const string KeyPrefetchCount = "prefetchcount";
public TapetiAppSettingsConnectionParams(string prefix = DefaultPrefix)
{
var keys = ConfigurationManager.AppSettings.AllKeys;
Action<string, Action<string>> getAppSetting = (key, setValue) =>
{
if (keys.Contains(prefix + key))
setValue(ConfigurationManager.AppSettings[prefix + key]);
};
getAppSetting(KeyHostname, value => HostName = value);
getAppSetting(KeyPort, value => Port = int.Parse(value));
getAppSetting(KeyVirtualHost, value => VirtualHost = value);
getAppSetting(KeyUsername, value => Username = value);
getAppSetting(KeyPassword, value => Password = value);
getAppSetting(KeyPrefetchCount, value => PrefetchCount = ushort.Parse(value));
}
}
}

View File

@ -150,7 +150,7 @@ namespace Tapeti
MessageClass = context.MessageClass, MessageClass = context.MessageClass,
MessageHandler = messageHandler, MessageHandler = messageHandler,
MessageMiddleware = context.MessageMiddleware, MessageMiddleware = context.MessageMiddleware,
BindingFilters = context.BindingFilters MessageFilterMiddleware = context.MessageFilterMiddleware
}; };
if (methodQueueInfo.Dynamic.GetValueOrDefault()) if (methodQueueInfo.Dynamic.GetValueOrDefault())
@ -268,11 +268,9 @@ 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 // TODO allow multiple only if there is a filter which guarantees uniqueness? and/or move to independant validation middleware
// TODO allow multiple, if there is a filter which guarantees uniqueness //if (existing.Any(h => h.MessageClass == binding.MessageClass))
// TODO move to independant validation middleware // throw new TopologyConfigurationException($"Multiple handlers for message class {binding.MessageClass.Name} in queue {binding.QueueInfo.Name}");
if (existing.Any(h => h.MessageClass == binding.MessageClass))
throw new TopologyConfigurationException($"Multiple handlers for message class {binding.MessageClass.Name} in queue {binding.QueueInfo.Name}");
existing.Add(binding); existing.Add(binding);
} }
@ -368,7 +366,7 @@ namespace Tapeti
public string QueueName { get; set; } public string QueueName { get; set; }
public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; } public IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; set; }
public IReadOnlyList<IBindingFilter> BindingFilters { get; set; } public IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; set; }
private QueueInfo queueInfo; private QueueInfo queueInfo;
public QueueInfo QueueInfo public QueueInfo QueueInfo
@ -390,21 +388,9 @@ namespace Tapeti
} }
public async Task<bool> Accept(IMessageContext context, object message) public bool Accept(IMessageContext context, object message)
{ {
if (message.GetType() != MessageClass) return message.GetType() == MessageClass;
return false;
if (BindingFilters == null)
return true;
foreach (var filter in BindingFilters)
{
if (!await filter.Accept(context, this))
return false;
}
return true;
} }
@ -431,7 +417,7 @@ namespace Tapeti
internal class BindingContext : IBindingContext internal class BindingContext : IBindingContext
{ {
private List<IMessageMiddleware> messageMiddleware; private List<IMessageMiddleware> messageMiddleware;
private List<IBindingFilter> bindingFilters; private List<IMessageFilterMiddleware> messageFilterMiddleware;
public Type MessageClass { get; set; } public Type MessageClass { get; set; }
@ -440,7 +426,7 @@ namespace Tapeti
public IBindingResult Result { get; } public IBindingResult Result { get; }
public IReadOnlyList<IMessageMiddleware> MessageMiddleware => messageMiddleware; public IReadOnlyList<IMessageMiddleware> MessageMiddleware => messageMiddleware;
public IReadOnlyList<IBindingFilter> BindingFilters => bindingFilters; public IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware => messageFilterMiddleware;
public BindingContext(MethodInfo method) public BindingContext(MethodInfo method)
@ -461,12 +447,12 @@ namespace Tapeti
} }
public void Use(IBindingFilter filter) public void Use(IMessageFilterMiddleware filterMiddleware)
{ {
if (bindingFilters == null) if (messageFilterMiddleware == null)
bindingFilters = new List<IBindingFilter>(); messageFilterMiddleware = new List<IMessageFilterMiddleware>();
bindingFilters.Add(filter); messageFilterMiddleware.Add(filterMiddleware);
} }
} }

View File

@ -7,8 +7,9 @@ using Tapeti.Flow.Annotations;
namespace Test namespace Test
{ {
[MessageController]
[DynamicQueue] [DynamicQueue]
public class MarcoController : MessageController public class MarcoController
{ {
private readonly IPublisher publisher; private readonly IPublisher publisher;
private readonly IFlowProvider flowProvider; private readonly IFlowProvider flowProvider;
@ -52,7 +53,6 @@ namespace Test
Console.WriteLine(message.ShouldMatchState.Equals(StateTestGuid) ? "Confirmed!" : "Oops! Mismatch!"); Console.WriteLine(message.ShouldMatchState.Equals(StateTestGuid) ? "Confirmed!" : "Oops! Mismatch!");
// This should error, as MarcoMessage expects a PoloMessage as a response
return flowProvider.EndWithResponse(new PoloMessage()); return flowProvider.EndWithResponse(new PoloMessage());
} }

View File

@ -17,9 +17,8 @@ namespace Test
public async Task Run() public async Task Run()
{ {
await publisher.Publish(new MarcoMessage()); // await publisher.Publish(new MarcoMessage());
/*
var concurrent = new SemaphoreSlim(20); var concurrent = new SemaphoreSlim(20);
while (true) while (true)
@ -37,14 +36,14 @@ namespace Test
} }
} }
await Task.Delay(1000); await Task.Delay(200);
} }
*/
/*
while (true) while (true)
{ {
await Task.Delay(1000); await Task.Delay(1000);
} }*/
} }
} }
} }

View File

@ -2,6 +2,7 @@
using SimpleInjector; using SimpleInjector;
using Tapeti; using Tapeti;
using Tapeti.Flow; using Tapeti.Flow;
using Tapeti.Flow.SQL;
using Tapeti.SimpleInjector; using Tapeti.SimpleInjector;
namespace Test namespace Test
@ -12,25 +13,23 @@ namespace Test
{ {
// TODO SQL based flow store // TODO SQL based flow store
// TODO logging // TODO logging
// TODO uitzoeken of we consumers kunnen pauzeren (denk: SQL down) --> nee, EFDBContext Get Async maken en retryen? kan dat, of timeout dan Rabbit?
var container = new Container(); var container = new Container();
container.Register<MarcoEmitter>(); container.Register<MarcoEmitter>();
container.Register<Visualizer>(); container.Register<Visualizer>();
container.Register<IFlowRepository>(); //container.Register<IFlowRepository>(() => new EF(serviceID));
var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container)) var config = new TapetiConfig(new SimpleInjectorDependencyResolver(container))
.WithFlow() .WithFlow()
//.WithFlowSqlRepository("data source=localhost;initial catalog=lef;integrated security=True;multipleactiveresultsets=True", 1)
.RegisterAllControllers() .RegisterAllControllers()
.Build(); .Build();
using (var connection = new TapetiConnection(config) using (var connection = new TapetiConnection(config)
{ {
Params = new TapetiConnectionParams Params = new TapetiAppSettingsConnectionParams()
{
HostName = "localhost",
PrefetchCount = 200
}
}) })
{ {
Console.WriteLine("Subscribing..."); Console.WriteLine("Subscribing...");

View File

@ -59,6 +59,10 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Tapeti.Flow.SQL\Tapeti.Flow.SQL.csproj">
<Project>{6de7b122-eb6a-46b8-aeaf-f84dde18f9c7}</Project>
<Name>Tapeti.Flow.SQL</Name>
</ProjectReference>
<ProjectReference Include="..\Tapeti\Tapeti.csproj"> <ProjectReference Include="..\Tapeti\Tapeti.csproj">
<Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project> <Project>{8ab4fd33-4aaa-465c-8579-9db3f3b23813}</Project>
<Name>Tapeti</Name> <Name>Tapeti</Name>