1
0
mirror of synced 2024-11-21 17:03:50 +00:00

[ci skip] Refactored how consume result is handled

Reimplemented the exception strategy and logging
Much XML documentation, such wow
This commit is contained in:
Mark van Renswoude 2019-08-14 12:20:53 +02:00
parent f8fca5879c
commit 6c32665c8a
52 changed files with 615 additions and 310 deletions

View File

@ -8,11 +8,6 @@ namespace Tapeti.Annotations
/// Binds to an existing durable queue to receive messages. Can be used
/// on an entire MessageController class or on individual methods.
/// </summary>
/// <remarks>
/// At the moment there is no support for creating a durable queue and managing the
/// bindings. The author recommends https://git.x2software.net/pub/RabbitMetaQueue
/// for deploy-time management of durable queues (shameless plug intended).
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)]
public class DurableQueueAttribute : Attribute

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
</Project>

View File

@ -1,9 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
// ReSharper disable UnusedMember.Global
namespace Tapeti.DataAnnotations.Extensions
{
/// <inheritdoc />
/// <summary>
/// Can be used on Guid fields which are supposed to be Required, as the Required attribute does
/// not work for Guids and making them Nullable is counter-intuitive.
@ -13,10 +15,12 @@ namespace Tapeti.DataAnnotations.Extensions
private const string DefaultErrorMessage = "'{0}' does not contain a valid guid";
private const string InvalidTypeErrorMessage = "'{0}' is not of type Guid";
/// <inheritdoc />
public RequiredGuidAttribute() : base(DefaultErrorMessage)
{
}
/// <inheritdoc />
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)

View File

@ -2,10 +2,11 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,10 +1,19 @@
namespace Tapeti.DataAnnotations
using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
/// <summary>
/// Extends ITapetiConfigBuilder to enable DataAnnotations.
/// </summary>
public static class ConfigExtensions
{
public static TapetiConfig WithDataAnnotations(this TapetiConfig config)
/// <summary>
/// Enables the DataAnnotations validation middleware.
/// </summary>
/// <param name="config"></param>
public static ITapetiConfigBuilder WithDataAnnotations(this ITapetiConfigBuilder config)
{
config.Use(new DataAnnotationsMiddleware());
config.Use(new DataAnnotationsExtension());
return config;
}
}

View File

@ -3,12 +3,18 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
public class DataAnnotationsMiddleware : ITapetiExtension
/// <inheritdoc />
/// <summary>
/// Provides the DataAnnotations validation middleware.
/// </summary>
public class DataAnnotationsExtension : ITapetiExtension
{
/// <inheritdoc />
public void RegisterDefaults(IDependencyContainer container)
{
}
/// <inheritdoc />
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{
return new object[]

View File

@ -5,8 +5,13 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
/// <inheritdoc />
/// <summary>
/// Validates consumed messages using System.ComponentModel.DataAnnotations
/// </summary>
public class DataAnnotationsMessageMiddleware : IMessageMiddleware
{
/// <inheritdoc />
public Task Handle(IMessageContext context, Func<Task> next)
{
var validationContext = new ValidationContext(context.Message);

View File

@ -5,8 +5,13 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations
{
/// <inheritdoc />
/// <summary>
/// Validates published messages using System.ComponentModel.DataAnnotations
/// </summary>
public class DataAnnotationsPublishMiddleware : IPublishMiddleware
{
/// <inheritdoc />
public Task Handle(IPublishContext context, Func<Task> next)
{
var validationContext = new ValidationContext(context.Message);

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -5,23 +5,32 @@ using Tapeti.Config;
namespace Tapeti.Flow.SQL
{
/// <summary>
/// Extends ITapetiConfigBuilder to enable Flow SQL.
/// </summary>
public static class ConfigExtensions
{
public static TapetiConfig WithFlowSqlRepository(this TapetiConfig config, string connectionString, string tableName = "Flow")
/// <summary>
/// Enables the Flow SQL repository.
/// </summary>
/// <param name="config"></param>
/// <param name="connectionString"></param>
/// <param name="tableName"></param>
public static ITapetiConfigBuilder WithFlowSqlRepository(this ITapetiConfigBuilder config, string connectionString, string tableName = "Flow")
{
config.Use(new FlowSqlRepositoryBundle(connectionString, tableName));
config.Use(new FlowSqlRepositoryExtension(connectionString, tableName));
return config;
}
}
internal class FlowSqlRepositoryBundle : ITapetiExtension
internal class FlowSqlRepositoryExtension : ITapetiExtension
{
private readonly string connectionString;
private readonly string tableName;
public FlowSqlRepositoryBundle(string connectionString, string tableName)
public FlowSqlRepositoryExtension(string connectionString, string tableName)
{
this.connectionString = connectionString;
this.tableName = tableName;

View File

@ -7,25 +7,27 @@ using Newtonsoft.Json;
namespace Tapeti.Flow.SQL
{
/*
Assumes the following table layout (table name configurable and may include schema):
create table Flow
(
FlowID uniqueidentifier not null,
CreationTime datetime2(3) not null,
StateJson nvarchar(max) null,
constraint PK_Flow primary key clustered (FlowID)
);
*/
/// <summary>
/// IFlowRepository implementation for SQL server.
/// </summary>
/// <remarks>
/// Assumes the following table layout (table name configurable and may include schema):
///
/// create table Flow
/// (
/// FlowID uniqueidentifier not null,
/// CreationTime datetime2(3) not null,
/// StateJson nvarchar(max) null,
/// constraint PK_Flow primary key clustered(FlowID)
/// );
/// </remarks>
public class SqlConnectionFlowRepository : IFlowRepository
{
private readonly string connectionString;
private readonly string tableName;
/// <inheritdoc />
public SqlConnectionFlowRepository(string connectionString, string tableName = "Flow")
{
this.connectionString = connectionString;
@ -33,6 +35,7 @@ namespace Tapeti.Flow.SQL
}
/// <inheritdoc />
public async Task<List<KeyValuePair<Guid, T>>> GetStates<T>()
{
using (var connection = await GetConnection())
@ -56,6 +59,7 @@ namespace Tapeti.Flow.SQL
}
/// <inheritdoc />
public async Task CreateState<T>(Guid flowID, T state, DateTime timestamp)
{
using (var connection = await GetConnection())
@ -76,6 +80,7 @@ namespace Tapeti.Flow.SQL
}
}
/// <inheritdoc />
public async Task UpdateState<T>(Guid flowID, T state)
{
using (var connection = await GetConnection())
@ -92,6 +97,7 @@ namespace Tapeti.Flow.SQL
}
}
/// <inheritdoc />
public async Task DeleteState(Guid flowID)
{
using (var connection = await GetConnection())

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,8 +1,10 @@
namespace Tapeti.Flow
using Tapeti.Config;
namespace Tapeti.Flow
{
public static class ConfigExtensions
{
public static TapetiConfig WithFlow(this TapetiConfig config, IFlowRepository flowRepository = null)
public static ITapetiConfigBuilder WithFlow(this ITapetiConfigBuilder config, IFlowRepository flowRepository = null)
{
config.Use(new FlowMiddleware(flowRepository));
return config;

View File

@ -44,7 +44,7 @@ namespace Tapeti.Flow.Default
}
public async Task Cleanup(IControllerMessageContext context, HandlingResult handlingResult, Func<Task> next)
public async Task Cleanup(IControllerMessageContext context, ConsumeResult consumeResult, Func<Task> next)
{
await next();
@ -53,11 +53,9 @@ namespace Tapeti.Flow.Default
if (flowContext?.FlowStateLock != null)
{
if (handlingResult.ConsumeResponse == ConsumeResponse.Nack
|| handlingResult.MessageAction == MessageAction.ErrorLog)
{
if (consumeResult == ConsumeResult.Error)
await flowContext.FlowStateLock.DeleteFlowState();
}
flowContext.FlowStateLock.Dispose();
}
}

View File

@ -42,7 +42,7 @@ namespace Tapeti.Flow.Default
}
private async Task CallControllerMethod<TController>(MethodInfo method, Func<object, Task<IYieldPoint>> getYieldPointResult, object[] parameters) where TController : class
private async Task CallControllerMethod<TController>(MethodBase method, Func<object, Task<IYieldPoint>> getYieldPointResult, object[] parameters) where TController : class
{
var controller = config.DependencyResolver.Resolve<TController>();
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
@ -55,24 +55,20 @@ namespace Tapeti.Flow.Default
var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>();
HandlingResultBuilder handlingResult = new HandlingResultBuilder
{
ConsumeResponse = ConsumeResponse.Nack,
};
try
{
await flowHandler.Execute(context, yieldPoint);
handlingResult.ConsumeResponse = ConsumeResponse.Ack;
//handlingResult.ConsumeResponse = ConsumeResponse.Ack;
}
finally
{
await RunCleanup(context, handlingResult.ToHandlingResult());
//await RunCleanup(context, handlingResult.ToHandlingResult());
}
}
/*
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
{
/*
foreach (var handler in config.CleanupMiddleware)
{
try
@ -84,8 +80,8 @@ namespace Tapeti.Flow.Default
logger.HandlerException(eCleanup);
}
}
*/
}
*/
private static MethodInfo GetExpressionMethod<TController, TResult>(Expression<Func<TController, Func<TResult>>> methodSelector)

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,27 +1,39 @@
using System;
using ISeriLogger = Serilog.ILogger;
using Tapeti.Config;
using ISerilogLogger = Serilog.ILogger;
// ReSharper disable UnusedMember.Global
namespace Tapeti.Serilog
{
/// <inheritdoc />
/// <summary>
/// Implements the Tapeti ILogger interface for Serilog output.
/// </summary>
public class TapetiSeriLogger: ILogger
{
private readonly ISeriLogger seriLogger;
private readonly ISerilogLogger seriLogger;
public TapetiSeriLogger(ISeriLogger seriLogger)
/// <inheritdoc />
public TapetiSeriLogger(ISerilogLogger seriLogger)
{
this.seriLogger = seriLogger;
}
public void Connect(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void Connect(TapetiConnectionParams connectionParams, bool isReconnect)
{
seriLogger.Information("Tapeti: trying to connect to {host}:{port}/{virtualHost}",
seriLogger
.ForContext("isReconnect", isReconnect)
.Information("Tapeti: trying to connect to {host}:{port}/{virtualHost}",
connectionParams.HostName,
connectionParams.Port,
connectionParams.VirtualHost);
}
/// <inheritdoc />
public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception)
{
seriLogger.Error(exception, "Tapeti: could not connect to {host}:{port}/{virtualHost}",
@ -30,17 +42,34 @@ namespace Tapeti.Serilog
connectionParams.VirtualHost);
}
public void ConnectSuccess(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void ConnectSuccess(TapetiConnectionParams connectionParams, bool isReconnect)
{
seriLogger.Information("Tapeti: successfully connected to {host}:{port}/{virtualHost}",
seriLogger
.ForContext("isReconnect", isReconnect)
.Information("Tapeti: successfully connected to {host}:{port}/{virtualHost}",
connectionParams.HostName,
connectionParams.Port,
connectionParams.VirtualHost);
}
public void HandlerException(Exception e)
/// <inheritdoc />
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
seriLogger.Error(e, "Tapeti: exception in message handler");
var contextLogger = seriLogger
.ForContext("consumeResult", consumeResult)
.ForContext("exchange", messageContext.Exchange)
.ForContext("queue", messageContext.Queue)
.ForContext("routingKey", messageContext.RoutingKey);
if (messageContext is IControllerMessageContext controllerMessageContext)
{
contextLogger = contextLogger
.ForContext("controller", controllerMessageContext.Binding.Controller.FullName)
.ForContext("method", controllerMessageContext.Binding.Method.Name);
}
contextLogger.Error(exception, "Tapeti: exception in message handler");
}
}
}

View File

@ -4,12 +4,17 @@ using SimpleInjector;
namespace Tapeti.SimpleInjector
{
/// <inheritdoc />
/// <summary>
/// Dependency resolver and container implementation for SimpleInjector.
/// </summary>
public class SimpleInjectorDependencyResolver : IDependencyContainer
{
private readonly Container container;
private readonly Lifestyle defaultsLifestyle;
private readonly Lifestyle controllersLifestyle;
/// <inheritdoc />
public SimpleInjectorDependencyResolver(Container container, Lifestyle defaultsLifestyle = null, Lifestyle controllersLifestyle = null)
{
this.container = container;
@ -17,17 +22,21 @@ namespace Tapeti.SimpleInjector
this.controllersLifestyle = controllersLifestyle;
}
/// <inheritdoc />
public T Resolve<T>() where T : class
{
return container.GetInstance<T>();
}
/// <inheritdoc />
public object Resolve(Type type)
{
return container.GetInstance(type);
}
/// <inheritdoc />
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
{
if (!CanRegisterDefault<TService>())
@ -39,6 +48,7 @@ namespace Tapeti.SimpleInjector
container.Register<TService, TImplementation>();
}
/// <inheritdoc />
public void RegisterDefault<TService>(Func<TService> factory) where TService : class
{
if (!CanRegisterDefault<TService>())
@ -50,24 +60,29 @@ namespace Tapeti.SimpleInjector
container.Register(factory);
}
/// <inheritdoc />
public void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService
{
if (CanRegisterDefault<TService>())
container.RegisterSingleton<TService, TImplementation>();
}
/// <inheritdoc />
public void RegisterDefaultSingleton<TService>(TService instance) where TService : class
{
if (CanRegisterDefault<TService>())
container.RegisterInstance(instance);
}
/// <inheritdoc />
public void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class
{
if (CanRegisterDefault<TService>())
container.RegisterSingleton(factory);
}
/// <inheritdoc />
public void RegisterController(Type type)
{
if (controllersLifestyle != null)

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1591</NoWarn>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -4,7 +4,7 @@ using Tapeti.Config;
namespace Tapeti.Transient
{
/// <summary>
/// TapetiConfig extension to register Tapeti.Transient
/// ITapetiConfigBuilder extension to register Tapeti.Transient
/// </summary>
public static class ConfigExtensions
{

View File

@ -13,8 +13,8 @@ namespace Tapeti.Config
/// Called after the message handler method, even if exceptions occured.
/// </summary>
/// <param name="context"></param>
/// <param name="handlingResult"></param>
/// <param name="consumeResult"></param>
/// <param name="next">Always call to allow the next in the chain to clean up</param>
Task Cleanup(IControllerMessageContext context, HandlingResult handlingResult, Func<Task> next);
Task Cleanup(IControllerMessageContext context, ConsumeResult consumeResult, Func<Task> next);
}
}

View File

@ -4,12 +4,26 @@
namespace Tapeti.Config
{
/// <summary>
/// Provides access to information about the message being consumed.
/// Allows the strategy to determine how the exception should be handled.
/// </summary>
public interface IExceptionStrategyContext
{
/// <summary>
/// Provides access to the message context.
/// </summary>
IMessageContext MessageContext { get; }
/// <summary>
/// Contains the exception being handled.
/// </summary>
Exception Exception { get; }
HandlingResultBuilder HandlingResult { get; set; }
/// <summary>
/// Determines how the message has been handled. Defaults to Error.
/// </summary>
/// <param name="consumeResult"></param>
void SetConsumeResult(ConsumeResult consumeResult);
}
}

View File

@ -3,8 +3,16 @@ using System.Threading.Tasks;
namespace Tapeti.Config
{
/// <summary>
/// Denotes middleware that processes all published messages.
/// </summary>
public interface IPublishMiddleware
{
/// <summary>
/// Called when a message is published.
/// </summary>
/// <param name="context"></param>
/// <param name="next">Call to pass the message to the next handler in the chain</param>
Task Handle(IPublishContext context, Func<Task> next);
}
}

View File

@ -12,11 +12,11 @@ namespace Tapeti.Connection
public class TapetiBasicConsumer : DefaultBasicConsumer
{
private readonly IConsumer consumer;
private readonly Func<ulong, ConsumeResponse, Task> onRespond;
private readonly Func<ulong, ConsumeResult, Task> onRespond;
/// <inheritdoc />
public TapetiBasicConsumer(IConsumer consumer, Func<ulong, ConsumeResponse, Task> onRespond)
public TapetiBasicConsumer(IConsumer consumer, Func<ulong, ConsumeResult, Task> onRespond)
{
this.consumer = consumer;
this.onRespond = onRespond;
@ -35,7 +35,7 @@ namespace Tapeti.Connection
}
catch
{
await onRespond(deliveryTag, ConsumeResponse.Nack);
await onRespond(deliveryTag, ConsumeResult.Error);
}
});
}

View File

@ -5,7 +5,6 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
@ -50,7 +49,7 @@ namespace Tapeti.Connection
private IModel channelInstance;
private ulong lastDeliveryTag;
private DateTime connectedDateTime;
private HttpClient managementClient;
private readonly HttpClient managementClient;
// These fields must be locked, since the callbacks for BasicAck/BasicReturn can run in a different thread
private readonly object confirmLock = new object();
@ -186,28 +185,29 @@ namespace Tapeti.Connection
}
private async Task Respond(ulong deliveryTag, ConsumeResponse response)
private async Task Respond(ulong deliveryTag, ConsumeResult result)
{
await taskQueue.Value.Add(() =>
{
// No need for a retryable channel here, if the connection is lost we can't
// use the deliveryTag anymore.
switch (response)
switch (result)
{
case ConsumeResponse.Ack:
case ConsumeResult.Success:
case ConsumeResult.ExternalRequeue:
GetChannel().BasicAck(deliveryTag, false);
break;
case ConsumeResponse.Nack:
case ConsumeResult.Error:
GetChannel().BasicNack(deliveryTag, false, false);
break;
case ConsumeResponse.Requeue:
case ConsumeResult.Requeue:
GetChannel().BasicNack(deliveryTag, false, true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(response), response, null);
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
});
@ -454,7 +454,7 @@ namespace Tapeti.Connection
{
try
{
logger.Connect(connectionParams);
logger.Connect(connectionParams, isReconnect);
connection = connectionFactory.CreateConnection();
channelInstance = connection.CreateModel();
@ -510,7 +510,7 @@ namespace Tapeti.Connection
else
ConnectionEventListener?.Connected();
logger.ConnectSuccess(connectionParams);
logger.ConnectSuccess(connectionParams, isReconnect);
isReconnect = true;
break;

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using Tapeti.Config;
using Tapeti.Default;
using System.Threading.Tasks;
@ -37,106 +38,49 @@ namespace Tapeti.Connection
/// <inheritdoc />
public async Task<ConsumeResponse> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
public async Task<ConsumeResult> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
{
object message = null;
try
{
var message = messageSerializer.Deserialize(body, properties);
message = messageSerializer.Deserialize(body, properties);
if (message == null)
throw new ArgumentException($"Message body could not be deserialized into a message object in queue {queueName}", nameof(body));
throw new ArgumentException("Message body could not be deserialized into a message object", nameof(body));
await DispatchMessage(message, new MessageContextData
return await DispatchMessage(message, new MessageContextData
{
Exchange = exchange,
RoutingKey = routingKey,
Properties = properties
});
return ConsumeResponse.Ack;
}
catch (Exception e)
catch (Exception dispatchException)
{
// TODO exception strategy
// TODO logger
return ConsumeResponse.Nack;
// TODO check if this is still necessary:
// var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch));
using (var emptyContext = new MessageContext
{
Config = config,
Queue = queueName,
Exchange = exchange,
RoutingKey = routingKey,
Message = message,
Properties = properties,
Binding = null
})
{
var exceptionContext = new ExceptionStrategyContext(emptyContext, dispatchException);
HandleException(exceptionContext);
return exceptionContext.ConsumeResult;
}
}
}
/*
handlingResult = new HandlingResult
{
ConsumeResponse = ConsumeResponse.Ack,
MessageAction = MessageAction.None
};
}
catch (Exception eDispatch)
{
var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch));
logger.HandlerException(eDispatch);
try
{
var exceptionStrategyContext = new ExceptionStrategyContext(context, exception.SourceException);
exceptionStrategy.HandleException(exceptionStrategyContext);
handlingResult = exceptionStrategyContext.HandlingResult.ToHandlingResult();
}
catch (Exception eStrategy)
{
logger.HandlerException(eStrategy);
}
}
try
{
if (handlingResult == null)
{
handlingResult = new HandlingResult
{
ConsumeResponse = ConsumeResponse.Nack,
MessageAction = MessageAction.None
};
}
await RunCleanup(context, handlingResult);
}
catch (Exception eCleanup)
{
logger.HandlerException(eCleanup);
}
}
finally
{
try
{
if (handlingResult == null)
{
handlingResult = new HandlingResult
{
ConsumeResponse = ConsumeResponse.Nack,
MessageAction = MessageAction.None
};
}
await client.Respond(deliveryTag, handlingResult.ConsumeResponse);
}
catch (Exception eRespond)
{
logger.HandlerException(eRespond);
}
try
{
context?.Dispose();
}
catch (Exception eDispose)
{
logger.HandlerException(eDispose);
}
}
*/
}
private async Task DispatchMessage(object message, MessageContextData messageContextData)
private async Task<ConsumeResult> DispatchMessage(object message, MessageContextData messageContextData)
{
var returnResult = ConsumeResult.Success;
var messageType = message.GetType();
var validMessageType = false;
@ -145,18 +89,23 @@ namespace Tapeti.Connection
if (!binding.Accept(messageType))
continue;
await InvokeUsingBinding(message, messageContextData, binding);
var consumeResult = await InvokeUsingBinding(message, messageContextData, binding);
validMessageType = true;
if (consumeResult != ConsumeResult.Success)
returnResult = consumeResult;
}
if (!validMessageType)
throw new ArgumentException($"Unsupported message type in queue {queueName}: {message.GetType().FullName}");
throw new ArgumentException($"No binding found for message type: {message.GetType().FullName}");
return returnResult;
}
private async Task InvokeUsingBinding(object message, MessageContextData messageContextData, IBinding binding)
private async Task<ConsumeResult> InvokeUsingBinding(object message, MessageContextData messageContextData, IBinding binding)
{
var context = new MessageContext
using (var context = new MessageContext
{
Config = config,
Queue = queueName,
@ -165,19 +114,42 @@ namespace Tapeti.Connection
Message = message,
Properties = messageContextData.Properties,
Binding = binding
};
})
{
try
{
await MiddlewareHelper.GoAsync(config.Middleware.Message,
(handler, next) => handler.Handle(context, next),
async () => { await binding.Invoke(context); });
return ConsumeResult.Success;
}
finally
catch (Exception invokeException)
{
context.Dispose();
var exceptionContext = new ExceptionStrategyContext(context, invokeException);
HandleException(exceptionContext);
return exceptionContext.ConsumeResult;
}
}
}
private void HandleException(ExceptionStrategyContext exceptionContext)
{
try
{
exceptionStrategy.HandleException(exceptionContext);
}
catch (Exception strategyException)
{
// Exception in the exception strategy. Oh dear.
exceptionContext.SetConsumeResult(ConsumeResult.Error);
logger.ConsumeException(strategyException, exceptionContext.MessageContext, ConsumeResult.Error);
}
logger.ConsumeException(exceptionContext.Exception, exceptionContext.MessageContext, exceptionContext.ConsumeResult);
}
private struct MessageContextData

View File

@ -1,23 +0,0 @@
namespace Tapeti
{
/// <summary>
/// Determines the response sent back after handling a message.
/// </summary>
public enum ConsumeResponse
{
/// <summary>
/// Acknowledge the message and remove it from the queue
/// </summary>
Ack,
/// <summary>
/// Negatively acknowledge the message and remove it from the queue, send to dead-letter queue if configured on the bus
/// </summary>
Nack,
/// <summary>
/// Negatively acknowledge the message and put it back in the queue to try again later
/// </summary>
Requeue
}
}

33
Tapeti/ConsumeResult.cs Normal file
View File

@ -0,0 +1,33 @@
namespace Tapeti
{
/// <summary>
/// Determines how the message has been handled and the response given to the message bus.
/// </summary>
public enum ConsumeResult
{
/// <summary>
/// Acknowledge the message and remove it from the queue.
/// </summary>
Success,
/// <summary>
/// Negatively acknowledge the message and remove it from the queue, send to dead-letter queue if configured on the bus.
/// </summary>
Error,
/// <summary>
/// Negatively acknowledge the message and put it back in the queue to try again later.
/// </summary>
Requeue,
/// <summary>
/// The message has been stored for republishing and will be delivered again by some other means.
/// It will be acknowledged and removed from the queue as if succesful.
/// </summary>
/// <remarks>
/// This option is for compatibility with external scheduler services. The exception strategy must guarantee that the
/// message will eventually be republished.
/// </remarks>
ExternalRequeue
}
}

View File

@ -1,27 +1,49 @@
using System;
using Tapeti.Config;
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// Default ILogger implementation for console applications.
/// </summary>
public class ConsoleLogger : ILogger
{
public void Connect(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void Connect(TapetiConnectionParams connectionParams, bool isReconnect)
{
Console.WriteLine($"[Tapeti] Connecting to {connectionParams.HostName}:{connectionParams.Port}{connectionParams.VirtualHost}");
Console.WriteLine($"[Tapeti] {(isReconnect ? "Reconnecting" : "Connecting")} to {connectionParams.HostName}:{connectionParams.Port}{connectionParams.VirtualHost}");
}
/// <inheritdoc />
public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception)
{
Console.WriteLine($"[Tapeti] Connection failed: {exception}");
}
public void ConnectSuccess(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void ConnectSuccess(TapetiConnectionParams connectionParams, bool isReconnect)
{
Console.WriteLine("[Tapeti] Connected");
Console.WriteLine($"[Tapeti] {(isReconnect ? "Reconnected" : "Connected")}");
}
public void HandlerException(Exception e)
/// <inheritdoc />
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
Console.WriteLine(e.ToString());
Console.WriteLine("[Tapeti] Exception while handling message");
Console.WriteLine($" Result : {consumeResult}");
Console.WriteLine($" Exchange : {messageContext.Exchange}");
Console.WriteLine($" Queue : {messageContext.Queue}");
Console.WriteLine($" RoutingKey : {messageContext.RoutingKey}");
if (messageContext is IControllerMessageContext controllerMessageContext)
{
Console.WriteLine($" Controller : {controllerMessageContext.Binding.Controller.FullName}");
Console.WriteLine($" Method : {controllerMessageContext.Binding.Method.Name}");
}
Console.WriteLine();
Console.WriteLine(exception);
}
}
}

View File

@ -7,7 +7,7 @@ namespace Tapeti.Default
/// <inheritdoc cref="IControllerMessageContext" />
public class ControllerMessageContext : MessageContext, IControllerMessageContext
{
private Dictionary<string, object> items = new Dictionary<string, object>();
private readonly Dictionary<string, object> items = new Dictionary<string, object>();
/// <inheritdoc />

View File

@ -1,22 +1,31 @@
using System;
using Tapeti.Config;
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// Default ILogger implementation which does not log anything.
/// </summary>
public class DevNullLogger : ILogger
{
public void Connect(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void Connect(TapetiConnectionParams connectionParams, bool isReconnect)
{
}
/// <inheritdoc />
public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception)
{
}
public void ConnectSuccess(TapetiConnectionParams connectionParams)
/// <inheritdoc />
public void ConnectSuccess(TapetiConnectionParams connectionParams, bool isReconnect)
{
}
public void HandlerException(Exception e)
/// <inheritdoc />
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
}
}

View File

@ -3,23 +3,37 @@ using Tapeti.Config;
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// Default implementation of IExceptionStrategyContext.
/// </summary>
public class ExceptionStrategyContext : IExceptionStrategyContext
{
internal ExceptionStrategyContext(IMessageContext messageContext, Exception exception)
/// <summary>
/// The ConsumeResult as set by the exception strategy. Defaults to Error.
/// </summary>
public ConsumeResult ConsumeResult { get; set; } = ConsumeResult.Error;
/// <inheritdoc />
public IMessageContext MessageContext { get; }
/// <inheritdoc />
public Exception Exception { get; }
/// <inheritdoc />
public ExceptionStrategyContext(IMessageContext messageContext, Exception exception)
{
MessageContext = messageContext;
Exception = exception;
}
public IMessageContext MessageContext { get; }
public Exception Exception { get; }
private HandlingResultBuilder handlingResult;
public HandlingResultBuilder HandlingResult
/// <inheritdoc />
public void SetConsumeResult(ConsumeResult consumeResult)
{
get => handlingResult ?? (handlingResult = new HandlingResultBuilder());
set => handlingResult = value;
ConsumeResult = consumeResult;
}
}
}

View File

@ -2,11 +2,16 @@
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// Default implementation of an exception strategy which marks the messages as Error.
/// </summary>
public class NackExceptionStrategy : IExceptionStrategy
{
/// <inheritdoc />
public void HandleException(IExceptionStrategyContext context)
{
context.HandlingResult.ConsumeResponse = ConsumeResponse.Nack;
context.SetConsumeResult(ConsumeResult.Error);
}
}
}

View File

@ -3,13 +3,20 @@ using System.Text.RegularExpressions;
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// IExchangeStrategy implementation which uses the first identifier in the namespace in lower case,
/// skipping the first identifier if it is 'Messaging'.
/// </summary>
/// <example>
/// Messaging.Service.Optional.Further.Parts will result in the exchange name 'service'.
/// </example>
public class NamespaceMatchExchangeStrategy : IExchangeStrategy
{
// If the namespace starts with "Messaging.Service[.Optional.Further.Parts]", the exchange will be "Service".
// If no Messaging prefix is present, the first part of the namespace will be used instead.
private static readonly Regex NamespaceRegex = new Regex("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
/// <inheritdoc />
public string GetExchange(Type messageType)
{
if (messageType.Namespace == null)

View File

@ -12,6 +12,9 @@ namespace Tapeti.Default
/// </summary>
public class RabbitMQMessageProperties : IMessageProperties
{
/// <summary>
/// Provides access to the wrapped IBasicProperties
/// </summary>
public IBasicProperties BasicProperties { get; }

View File

@ -4,11 +4,25 @@
namespace Tapeti.Default
{
/// <inheritdoc />
/// <summary>
/// Example exception strategy which requeues all messages that result in an error.
/// </summary>
/// <remarks>
/// You probably do not want to use this strategy as-is in production code, unless
/// you are sure that all your exceptions are transient. A better way would be to
/// check for exceptions you know are transient. An even better way would be to
/// never requeue but retry transient errors internally. See the Tapeti documentation
/// for an example of this pattern:
///
/// https://tapeti.readthedocs.io/en/latest/
/// </remarks>
public class RequeueExceptionStrategy : IExceptionStrategy
{
/// <inheritdoc />
public void HandleException(IExceptionStrategyContext context)
{
context.HandlingResult.ConsumeResponse = ConsumeResponse.Requeue;
context.SetConsumeResult(ConsumeResult.Requeue);
}
}
}

View File

@ -6,6 +6,13 @@ using System.Text.RegularExpressions;
namespace Tapeti.Default
{
/// <summary>
/// IRoutingKeyStrategy implementation which transforms the class name into a dot-separated routing key based
/// on the casing. Accounts for acronyms. If the class name ends with 'Message' it is not included in the routing key.
/// </summary>
/// <example>
/// ExampleClassNameMessage will result in example.class.name
/// </example>
public class TypeNameRoutingKeyStrategy : IRoutingKeyStrategy
{
private const string SeparatorPattern = @"
@ -24,12 +31,17 @@ namespace Tapeti.Default
private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new ConcurrentDictionary<Type, string>();
/// <inheritdoc />
public string GetRoutingKey(Type messageType)
{
return RoutingKeyCache.GetOrAdd(messageType, BuildRoutingKey);
}
/// <summary>
/// Actual implementation of GetRoutingKey, called only when the type has not been cached yet.
/// </summary>
/// <param name="messageType"></param>
protected virtual string BuildRoutingKey(Type messageType)
{
// Split PascalCase into dot-separated parts. If the class name ends in "Message" leave that out.
@ -43,6 +55,7 @@ namespace Tapeti.Default
return string.Join(".", words.Select(s => s.ToLower()));
}
private static List<string> SplitPascalCase(string value)
{
var split = SeparatorRegex.Split(value);

View File

@ -2,8 +2,13 @@
namespace Tapeti.Exceptions
{
/// <inheritdoc />
/// <summary>
/// Raised when a message is nacked by the message bus.
/// </summary>
public class NackException : Exception
{
/// <inheritdoc />
public NackException(string message) : base(message) { }
}
}

View File

@ -2,8 +2,13 @@
namespace Tapeti.Exceptions
{
/// <inheritdoc />
/// <summary>
/// Raised when a mandatory message has no route.
/// </summary>
public class NoRouteException : Exception
{
/// <inheritdoc />
public NoRouteException(string message) : base(message) { }
}
}

View File

@ -1,63 +0,0 @@
// ReSharper disable UnusedMember.Global
namespace Tapeti
{
public class HandlingResult
{
public HandlingResult()
{
ConsumeResponse = ConsumeResponse.Nack;
MessageAction = MessageAction.None;
}
/// <summary>
/// Determines which response will be given to the message bus from where the message originates.
/// </summary>
public ConsumeResponse ConsumeResponse { get; internal set; }
/// <summary>
/// Registers which action the Exception strategy has taken or will take to handle the error condition
/// on the message. This is important to know for cleanup handlers registered by middleware.
/// </summary>
public MessageAction MessageAction { get; internal set; }
}
public class HandlingResultBuilder
{
private static readonly HandlingResult Default = new HandlingResult();
private HandlingResult data = Default;
public ConsumeResponse ConsumeResponse {
get => data.ConsumeResponse;
set => GetWritableData().ConsumeResponse = value;
}
public MessageAction MessageAction
{
get => data.MessageAction;
set => GetWritableData().MessageAction = value;
}
public HandlingResult ToHandlingResult()
{
if (data == Default)
{
return new HandlingResult();
}
var result = GetWritableData();
data = Default;
return result;
}
private HandlingResult GetWritableData()
{
if (data == Default)
{
data = new HandlingResult();
}
return data;
}
}
}

View File

@ -2,9 +2,17 @@
namespace Tapeti.Helpers
{
/// <summary>
/// Helper class for console applications.
/// </summary>
public static class ConsoleHelper
{
// Source: http://stackoverflow.com/questions/6408588/how-to-tell-if-there-is-a-console
/// <summary>
/// Determines if the application is running in a console.
/// </summary>
/// <remarks>
/// Source: http://stackoverflow.com/questions/6408588/how-to-tell-if-there-is-a-console
/// </remarks>
public static bool IsAvailable()
{
try

View File

@ -4,8 +4,18 @@ using System.Threading.Tasks;
namespace Tapeti.Helpers
{
/// <summary>
/// Helper class for executing the middleware pattern.
/// </summary>
public static class MiddlewareHelper
{
/// <summary>
/// Executes the chain of middleware synchronously, starting with the last item in the list.
/// </summary>
/// <param name="middleware">The list of middleware to run</param>
/// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param>
/// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param>
/// <typeparam name="T"></typeparam>
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler)
{
var handlerIndex = middleware?.Count - 1 ?? -1;
@ -28,6 +38,13 @@ namespace Tapeti.Helpers
}
/// <summary>
/// Executes the chain of middleware asynchronously, starting with the last item in the list.
/// </summary>
/// <param name="middleware">The list of middleware to run</param>
/// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param>
/// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param>
/// <typeparam name="T"></typeparam>
public static async Task GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<Task>, Task> handle, Func<Task> lastHandler)
{
var handlerIndex = middleware?.Count - 1 ?? -1;

View File

@ -3,8 +3,18 @@ using System.Threading.Tasks;
namespace Tapeti.Helpers
{
/// <summary>
/// Helper methods for working with synchronous and asynchronous versions of methods.
/// </summary>
public static class TaskTypeHelper
{
/// <summary>
/// Determines if the given type matches the predicate, taking Task types into account.
/// </summary>
/// <param name="type"></param>
/// <param name="predicate"></param>
/// <param name="isTaskOf"></param>
/// <param name="actualType"></param>
public static bool IsTypeOrTaskOf(this Type type, Func<Type, bool> predicate, out bool isTaskOf, out Type actualType)
{
if (type == typeof(Task))
@ -32,11 +42,24 @@ namespace Tapeti.Helpers
}
/// <summary>
/// Determines if the given type matches the predicate, taking Task types into account.
/// </summary>
/// <param name="type"></param>
/// <param name="predicate"></param>
/// <param name="isTaskOf"></param>
public static bool IsTypeOrTaskOf(this Type type, Func<Type, bool> predicate, out bool isTaskOf)
{
return IsTypeOrTaskOf(type, predicate, out isTaskOf, out _);
}
/// <summary>
/// Determines if the given type matches the compareTo type, taking Task types into account.
/// </summary>
/// <param name="type"></param>
/// <param name="compareTo"></param>
/// <param name="isTaskOf"></param>
public static bool IsTypeOrTaskOf(this Type type, Type compareTo, out bool isTaskOf)
{
return IsTypeOrTaskOf(type, t => t == compareTo, out isTaskOf);

View File

@ -16,6 +16,6 @@ namespace Tapeti
/// <param name="properties">Metadata included in the message</param>
/// <param name="body">The raw body of the message</param>
/// <returns></returns>
Task<ConsumeResponse> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body);
Task<ConsumeResult> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body);
}
}

View File

@ -7,24 +7,79 @@ namespace Tapeti
/// </summary>
public interface IDependencyResolver
{
/// <summary>
/// Resolve an instance of T
/// </summary>
/// <typeparam name="T">The type to instantiate</typeparam>
/// <returns>A new or singleton instance, depending on the registration</returns>
T Resolve<T>() where T : class;
/// <summary>
/// Resolve an instance of T
/// </summary>
/// <param name="type">The type to instantiate</param>
/// <returns>A new or singleton instance, depending on the registration</returns>
object Resolve(Type type);
}
/// <inheritdoc />
/// <summary>
/// Allows registering controller classes into the IoC container. Also registers default implementations,
/// so that the calling application may override these.
/// </summary>
/// <remarks>
/// All implementations of IDependencyResolver should implement IDependencyContainer as well,
/// otherwise all registrations of Tapeti components will have to be done manually by the application.
/// </remarks>
public interface IDependencyContainer : IDependencyResolver
{
/// <summary>
/// Registers a default implementation in the IoC container. If an alternative implementation
/// was registered before, it is not replaced.
/// </summary>
/// <typeparam name="TService"></typeparam>
/// <typeparam name="TImplementation"></typeparam>
void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService;
/// <summary>
/// Registers a default implementation in the IoC container. If an alternative implementation
/// was registered before, it is not replaced.
/// </summary>
/// <param name="factory"></param>
/// <typeparam name="TService"></typeparam>
void RegisterDefault<TService>(Func<TService> factory) where TService : class;
/// <summary>
/// Registers a default singleton implementation in the IoC container. If an alternative implementation
/// was registered before, it is not replaced.
/// </summary>
/// <typeparam name="TService"></typeparam>
/// <typeparam name="TImplementation"></typeparam>
void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService;
/// <summary>
/// Registers a default singleton implementation in the IoC container. If an alternative implementation
/// was registered before, it is not replaced.
/// </summary>
/// <param name="instance"></param>
/// <typeparam name="TService"></typeparam>
void RegisterDefaultSingleton<TService>(TService instance) where TService : class;
/// <summary>
/// Registers a default singleton implementation in the IoC container. If an alternative implementation
/// was registered before, it is not replaced.
/// </summary>
/// <param name="factory"></param>
/// <typeparam name="TService"></typeparam>
void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class;
/// <summary>
/// Registers a concrete controller class in the IoC container.
/// </summary>
/// <param name="type"></param>
void RegisterController(Type type);
}
}

View File

@ -2,14 +2,16 @@
namespace Tapeti
{
/// <summary>
/// Called when an exception occurs while handling a message. Determines how it should be handled.
/// </summary>
public interface IExceptionStrategy
{
/// <summary>
/// Called when an exception occurs while handling a message.
/// </summary>
/// <param name="context">The exception strategy context containing the necessary data including the message context and the thrown exception.
/// Also the response to the message can be set.
/// If there is any other handling of the message than the expected default than HandlingResult.MessageFutureAction must be set accordingly. </param>
/// Also proivdes methods for the exception strategy to indicate how the message should be handled.</param>
void HandleException(IExceptionStrategyContext context);
}
}

View File

@ -2,8 +2,16 @@
namespace Tapeti
{
/// <summary>
/// Translates message classes into their target exchange.
/// </summary>
public interface IExchangeStrategy
{
/// <summary>
/// Determines the exchange belonging to the given message class.
/// </summary>
/// <param name="messageType"></param>
/// <returns></returns>
string GetExchange(Type messageType);
}
}

View File

@ -1,16 +1,46 @@
using System;
using Tapeti.Config;
// ReSharper disable UnusedMember.Global
namespace Tapeti
{
// This interface is deliberately specific and typed to allow for structured logging (e.g. Serilog)
// instead of only string-based logging without control over the output.
/// <summary>
/// Handles the logging of various events in Tapeti
/// </summary>
/// <remarks>
/// This interface is deliberately specific and typed to allow for structured logging (e.g. Serilog)
/// instead of only string-based logging without control over the output.
/// </remarks>
public interface ILogger
{
void Connect(TapetiConnectionParams connectionParams);
/// <summary>
/// Called before a connection to RabbitMQ is attempted.
/// </summary>
/// <param name="connectionParams"></param>
/// <param name="isReconnect">Indicates whether this is the initial connection or a reconnect</param>
void Connect(TapetiConnectionParams connectionParams, bool isReconnect);
/// <summary>
/// Called when the connection has failed or is lost.
/// </summary>
/// <param name="connectionParams"></param>
/// <param name="exception"></param>
void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception);
void ConnectSuccess(TapetiConnectionParams connectionParams);
void HandlerException(Exception e);
/// <summary>
/// Called when a connection to RabbitMQ has been succesfully established.
/// </summary>
/// <param name="connectionParams"></param>
/// <param name="isReconnect">Indicates whether this is the initial connection or a reconnect</param>
void ConnectSuccess(TapetiConnectionParams connectionParams, bool isReconnect);
/// <summary>
/// Called when an exception occurs in a consumer.
/// </summary>
/// <param name="exception"></param>
/// <param name="messageContext"></param>
/// <param name="consumeResult">Indicates the action taken by the exception handler</param>
void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult);
}
}

View File

@ -2,8 +2,16 @@
namespace Tapeti
{
/// <summary>
/// Translates message classes into routing keys.
/// </summary>
public interface IRoutingKeyStrategy
{
/// <summary>
/// Determines the routing key for the given message class.
/// </summary>
/// <param name="messageType"></param>
/// <returns></returns>
string GetRoutingKey(Type messageType);
}
}

29
Tapeti/MessageAction.cs Normal file
View File

@ -0,0 +1,29 @@
// ReSharper disable UnusedMember.Global
namespace Tapeti
{
/// <summary>
/// Indicates how the message was handled.
/// </summary>
public enum MessageAction
{
/// <summary>
/// The message was handled succesfully.
/// </summary>
Success,
/// <summary>
/// There was an error while processing the message.
/// </summary>
Error,
/// <summary>
/// The message has been stored for republishing and will be delivered again
/// even if the current messages has been Acked or Nacked.
/// </summary>
/// <remarks>
/// This option is for compatibility with external scheduler services that do not immediately requeue a message.
/// </remarks>
ExternalRetry
}
}

View File

@ -1,11 +0,0 @@
// ReSharper disable UnusedMember.Global
namespace Tapeti
{
public enum MessageAction
{
None = 1,
ErrorLog = 2,
Retry = 3,
}
}

View File

@ -4,17 +4,35 @@ using System.Linq;
namespace Tapeti
{
/// <inheritdoc />
/// <summary>
/// Implementation of TapetiConnectionParams which reads the values from the AppSettings.
/// </summary>
/// <list type="table">
/// <listheader>
/// <description>AppSettings keys</description>
/// </listheader>
/// <item><description>rabbitmq:hostname</description></item>
/// <item><description>rabbitmq:port</description></item>
/// <item><description>rabbitmq:virtualhost</description></item>
/// <item><description>rabbitmq:username</description></item>
/// <item><description>rabbitmq:password</description></item>
/// <item><description>rabbitmq:prefetchcount</description></item>
/// </list>
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";
private const string DefaultPrefix = "rabbitmq:";
private const string KeyHostname = "hostname";
private const string KeyPort = "port";
private const string KeyVirtualHost = "virtualhost";
private const string KeyUsername = "username";
private const string KeyPassword = "password";
private const string KeyPrefetchCount = "prefetchcount";
/// <inheritdoc />
/// <summary></summary>
/// <param name="prefix">The prefix to apply to the keys. Defaults to "rabbitmq:"</param>
public TapetiAppSettingsConnectionParams(string prefix = DefaultPrefix)
{
var keys = ConfigurationManager.AppSettings.AllKeys;