[ci skip] Refactored how consume result is handled
Reimplemented the exception strategy and logging Much XML documentation, such wow
This commit is contained in:
parent
f8fca5879c
commit
6c32665c8a
@ -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
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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[]
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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}",
|
||||
connectionParams.HostName,
|
||||
connectionParams.Port,
|
||||
connectionParams.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}",
|
||||
connectionParams.HostName,
|
||||
connectionParams.Port,
|
||||
connectionParams.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
<NoWarn>1701;1702</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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
|
||||
{
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
||||
|
||||
/*
|
||||
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Ack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
catch (Exception eDispatch)
|
||||
using (var emptyContext = new MessageContext
|
||||
{
|
||||
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
|
||||
Config = config,
|
||||
Queue = queueName,
|
||||
Exchange = exchange,
|
||||
RoutingKey = routingKey,
|
||||
Message = message,
|
||||
Properties = properties,
|
||||
Binding = null
|
||||
})
|
||||
{
|
||||
if (handlingResult == null)
|
||||
{
|
||||
handlingResult = new HandlingResult
|
||||
{
|
||||
ConsumeResponse = ConsumeResponse.Nack,
|
||||
MessageAction = MessageAction.None
|
||||
};
|
||||
}
|
||||
await RunCleanup(context, handlingResult);
|
||||
}
|
||||
catch (Exception eCleanup)
|
||||
{
|
||||
logger.HandlerException(eCleanup);
|
||||
var exceptionContext = new ExceptionStrategyContext(emptyContext, dispatchException);
|
||||
HandleException(exceptionContext);
|
||||
return exceptionContext.ConsumeResult;
|
||||
}
|
||||
}
|
||||
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,21 +114,44 @@ 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); });
|
||||
|
||||
try
|
||||
{
|
||||
await MiddlewareHelper.GoAsync(config.Middleware.Message,
|
||||
(handler, next) => handler.Handle(context, next),
|
||||
async () => { await binding.Invoke(context); });
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Dispose();
|
||||
return ConsumeResult.Success;
|
||||
}
|
||||
catch (Exception invokeException)
|
||||
{
|
||||
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
|
||||
{
|
||||
public string Exchange;
|
||||
|
@ -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
33
Tapeti/ConsumeResult.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -12,6 +12,9 @@ namespace Tapeti.Default
|
||||
/// </summary>
|
||||
public class RabbitMQMessageProperties : IMessageProperties
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to the wrapped IBasicProperties
|
||||
/// </summary>
|
||||
public IBasicProperties BasicProperties { get; }
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
29
Tapeti/MessageAction.cs
Normal 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
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Tapeti
|
||||
{
|
||||
public enum MessageAction
|
||||
{
|
||||
None = 1,
|
||||
ErrorLog = 2,
|
||||
Retry = 3,
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user