[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 /// Binds to an existing durable queue to receive messages. Can be used
/// on an entire MessageController class or on individual methods. /// on an entire MessageController class or on individual methods.
/// </summary> /// </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)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)]
public class DurableQueueAttribute : Attribute public class DurableQueueAttribute : Attribute

View File

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

View File

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

View File

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

View File

@ -3,12 +3,18 @@ using Tapeti.Config;
namespace Tapeti.DataAnnotations 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) public void RegisterDefaults(IDependencyContainer container)
{ {
} }
/// <inheritdoc />
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver) public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{ {
return new object[] return new object[]

View File

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

View File

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

View File

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

View File

@ -5,23 +5,32 @@ using Tapeti.Config;
namespace Tapeti.Flow.SQL namespace Tapeti.Flow.SQL
{ {
/// <summary>
/// Extends ITapetiConfigBuilder to enable Flow SQL.
/// </summary>
public static class ConfigExtensions 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; return config;
} }
} }
internal class FlowSqlRepositoryBundle : ITapetiExtension internal class FlowSqlRepositoryExtension : ITapetiExtension
{ {
private readonly string connectionString; private readonly string connectionString;
private readonly string tableName; private readonly string tableName;
public FlowSqlRepositoryBundle(string connectionString, string tableName) public FlowSqlRepositoryExtension(string connectionString, string tableName)
{ {
this.connectionString = connectionString; this.connectionString = connectionString;
this.tableName = tableName; this.tableName = tableName;

View File

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

View File

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

View File

@ -1,8 +1,10 @@
namespace Tapeti.Flow using Tapeti.Config;
namespace Tapeti.Flow
{ {
public static class ConfigExtensions 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)); config.Use(new FlowMiddleware(flowRepository));
return config; 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(); await next();
@ -53,11 +53,9 @@ namespace Tapeti.Flow.Default
if (flowContext?.FlowStateLock != null) if (flowContext?.FlowStateLock != null)
{ {
if (handlingResult.ConsumeResponse == ConsumeResponse.Nack if (consumeResult == ConsumeResult.Error)
|| handlingResult.MessageAction == MessageAction.ErrorLog)
{
await flowContext.FlowStateLock.DeleteFlowState(); await flowContext.FlowStateLock.DeleteFlowState();
}
flowContext.FlowStateLock.Dispose(); 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 controller = config.DependencyResolver.Resolve<TController>();
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters)); var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
@ -55,24 +55,20 @@ namespace Tapeti.Flow.Default
var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>(); var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>();
HandlingResultBuilder handlingResult = new HandlingResultBuilder
{
ConsumeResponse = ConsumeResponse.Nack,
};
try try
{ {
await flowHandler.Execute(context, yieldPoint); await flowHandler.Execute(context, yieldPoint);
handlingResult.ConsumeResponse = ConsumeResponse.Ack; //handlingResult.ConsumeResponse = ConsumeResponse.Ack;
} }
finally finally
{ {
await RunCleanup(context, handlingResult.ToHandlingResult()); //await RunCleanup(context, handlingResult.ToHandlingResult());
} }
} }
/*
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult) private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
{ {
/*
foreach (var handler in config.CleanupMiddleware) foreach (var handler in config.CleanupMiddleware)
{ {
try try
@ -84,8 +80,8 @@ namespace Tapeti.Flow.Default
logger.HandlerException(eCleanup); logger.HandlerException(eCleanup);
} }
} }
*/
} }
*/
private static MethodInfo GetExpressionMethod<TController, TResult>(Expression<Func<TController, Func<TResult>>> methodSelector) private static MethodInfo GetExpressionMethod<TController, TResult>(Expression<Func<TController, Func<TResult>>> methodSelector)

View File

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

View File

@ -1,27 +1,39 @@
using System; using System;
using ISeriLogger = Serilog.ILogger; using Tapeti.Config;
using ISerilogLogger = Serilog.ILogger;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
namespace Tapeti.Serilog namespace Tapeti.Serilog
{ {
/// <inheritdoc />
/// <summary>
/// Implements the Tapeti ILogger interface for Serilog output.
/// </summary>
public class TapetiSeriLogger: ILogger public class TapetiSeriLogger: ILogger
{ {
private readonly ISeriLogger seriLogger; private readonly ISerilogLogger seriLogger;
public TapetiSeriLogger(ISeriLogger seriLogger)
/// <inheritdoc />
public TapetiSeriLogger(ISerilogLogger seriLogger)
{ {
this.seriLogger = 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
connectionParams.HostName, .ForContext("isReconnect", isReconnect)
connectionParams.Port, .Information("Tapeti: trying to connect to {host}:{port}/{virtualHost}",
connectionParams.VirtualHost); connectionParams.HostName,
connectionParams.Port,
connectionParams.VirtualHost);
} }
/// <inheritdoc />
public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception) public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception)
{ {
seriLogger.Error(exception, "Tapeti: could not connect to {host}:{port}/{virtualHost}", seriLogger.Error(exception, "Tapeti: could not connect to {host}:{port}/{virtualHost}",
@ -30,17 +42,34 @@ namespace Tapeti.Serilog
connectionParams.VirtualHost); 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
connectionParams.HostName, .ForContext("isReconnect", isReconnect)
connectionParams.Port, .Information("Tapeti: successfully connected to {host}:{port}/{virtualHost}",
connectionParams.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 namespace Tapeti.SimpleInjector
{ {
/// <inheritdoc />
/// <summary>
/// Dependency resolver and container implementation for SimpleInjector.
/// </summary>
public class SimpleInjectorDependencyResolver : IDependencyContainer public class SimpleInjectorDependencyResolver : IDependencyContainer
{ {
private readonly Container container; private readonly Container container;
private readonly Lifestyle defaultsLifestyle; private readonly Lifestyle defaultsLifestyle;
private readonly Lifestyle controllersLifestyle; private readonly Lifestyle controllersLifestyle;
/// <inheritdoc />
public SimpleInjectorDependencyResolver(Container container, Lifestyle defaultsLifestyle = null, Lifestyle controllersLifestyle = null) public SimpleInjectorDependencyResolver(Container container, Lifestyle defaultsLifestyle = null, Lifestyle controllersLifestyle = null)
{ {
this.container = container; this.container = container;
@ -17,17 +22,21 @@ namespace Tapeti.SimpleInjector
this.controllersLifestyle = controllersLifestyle; this.controllersLifestyle = controllersLifestyle;
} }
/// <inheritdoc />
public T Resolve<T>() where T : class public T Resolve<T>() where T : class
{ {
return container.GetInstance<T>(); return container.GetInstance<T>();
} }
/// <inheritdoc />
public object Resolve(Type type) public object Resolve(Type type)
{ {
return container.GetInstance(type); return container.GetInstance(type);
} }
/// <inheritdoc />
public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService public void RegisterDefault<TService, TImplementation>() where TService : class where TImplementation : class, TService
{ {
if (!CanRegisterDefault<TService>()) if (!CanRegisterDefault<TService>())
@ -39,6 +48,7 @@ namespace Tapeti.SimpleInjector
container.Register<TService, TImplementation>(); container.Register<TService, TImplementation>();
} }
/// <inheritdoc />
public void RegisterDefault<TService>(Func<TService> factory) where TService : class public void RegisterDefault<TService>(Func<TService> factory) where TService : class
{ {
if (!CanRegisterDefault<TService>()) if (!CanRegisterDefault<TService>())
@ -50,24 +60,29 @@ namespace Tapeti.SimpleInjector
container.Register(factory); container.Register(factory);
} }
/// <inheritdoc />
public void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService public void RegisterDefaultSingleton<TService, TImplementation>() where TService : class where TImplementation : class, TService
{ {
if (CanRegisterDefault<TService>()) if (CanRegisterDefault<TService>())
container.RegisterSingleton<TService, TImplementation>(); container.RegisterSingleton<TService, TImplementation>();
} }
/// <inheritdoc />
public void RegisterDefaultSingleton<TService>(TService instance) where TService : class public void RegisterDefaultSingleton<TService>(TService instance) where TService : class
{ {
if (CanRegisterDefault<TService>()) if (CanRegisterDefault<TService>())
container.RegisterInstance(instance); container.RegisterInstance(instance);
} }
/// <inheritdoc />
public void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class public void RegisterDefaultSingleton<TService>(Func<TService> factory) where TService : class
{ {
if (CanRegisterDefault<TService>()) if (CanRegisterDefault<TService>())
container.RegisterSingleton(factory); container.RegisterSingleton(factory);
} }
/// <inheritdoc />
public void RegisterController(Type type) public void RegisterController(Type type)
{ {
if (controllersLifestyle != null) if (controllersLifestyle != null)

View File

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

View File

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

View File

@ -13,8 +13,8 @@ namespace Tapeti.Config
/// Called after the message handler method, even if exceptions occured. /// Called after the message handler method, even if exceptions occured.
/// </summary> /// </summary>
/// <param name="context"></param> /// <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> /// <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 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 public interface IExceptionStrategyContext
{ {
/// <summary>
/// Provides access to the message context.
/// </summary>
IMessageContext MessageContext { get; } IMessageContext MessageContext { get; }
/// <summary>
/// Contains the exception being handled.
/// </summary>
Exception Exception { get; } 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 namespace Tapeti.Config
{ {
/// <summary>
/// Denotes middleware that processes all published messages.
/// </summary>
public interface IPublishMiddleware 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); Task Handle(IPublishContext context, Func<Task> next);
} }
} }

View File

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

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.ExceptionServices;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Default; using Tapeti.Default;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -37,106 +38,49 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <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 try
{ {
var message = messageSerializer.Deserialize(body, properties); message = messageSerializer.Deserialize(body, properties);
if (message == null) 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, Exchange = exchange,
RoutingKey = routingKey, RoutingKey = routingKey,
Properties = properties Properties = properties
}); });
return ConsumeResponse.Ack;
} }
catch (Exception e) catch (Exception dispatchException)
{ {
// TODO exception strategy // TODO check if this is still necessary:
// TODO logger // var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch));
return ConsumeResponse.Nack;
}
using (var emptyContext = new MessageContext
/*
handlingResult = new HandlingResult
{
ConsumeResponse = ConsumeResponse.Ack,
MessageAction = MessageAction.None
};
}
catch (Exception eDispatch)
{ {
var exception = ExceptionDispatchInfo.Capture(UnwrapException(eDispatch)); Config = config,
logger.HandlerException(eDispatch); Queue = queueName,
try Exchange = exchange,
{ RoutingKey = routingKey,
var exceptionStrategyContext = new ExceptionStrategyContext(context, exception.SourceException); Message = message,
Properties = properties,
exceptionStrategy.HandleException(exceptionStrategyContext); Binding = null
})
handlingResult = exceptionStrategyContext.HandlingResult.ToHandlingResult();
}
catch (Exception eStrategy)
{
logger.HandlerException(eStrategy);
}
}
try
{ {
if (handlingResult == null) var exceptionContext = new ExceptionStrategyContext(emptyContext, dispatchException);
{ HandleException(exceptionContext);
handlingResult = new HandlingResult return exceptionContext.ConsumeResult;
{
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 messageType = message.GetType();
var validMessageType = false; var validMessageType = false;
@ -145,18 +89,23 @@ namespace Tapeti.Connection
if (!binding.Accept(messageType)) if (!binding.Accept(messageType))
continue; continue;
await InvokeUsingBinding(message, messageContextData, binding); var consumeResult = await InvokeUsingBinding(message, messageContextData, binding);
validMessageType = true; validMessageType = true;
if (consumeResult != ConsumeResult.Success)
returnResult = consumeResult;
} }
if (!validMessageType) 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, Config = config,
Queue = queueName, Queue = queueName,
@ -165,21 +114,44 @@ namespace Tapeti.Connection
Message = message, Message = message,
Properties = messageContextData.Properties, Properties = messageContextData.Properties,
Binding = binding Binding = binding
}; })
{
try
{
await MiddlewareHelper.GoAsync(config.Middleware.Message,
(handler, next) => handler.Handle(context, next),
async () => { await binding.Invoke(context); });
try return ConsumeResult.Success;
{ }
await MiddlewareHelper.GoAsync(config.Middleware.Message, catch (Exception invokeException)
(handler, next) => handler.Handle(context, next), {
async () => { await binding.Invoke(context); }); var exceptionContext = new ExceptionStrategyContext(context, invokeException);
} HandleException(exceptionContext);
finally return exceptionContext.ConsumeResult;
{ }
context.Dispose();
} }
} }
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 private struct MessageContextData
{ {
public string Exchange; public string Exchange;

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 System;
using Tapeti.Config;
namespace Tapeti.Default namespace Tapeti.Default
{ {
/// <inheritdoc />
/// <summary>
/// Default ILogger implementation for console applications.
/// </summary>
public class ConsoleLogger : ILogger 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) public void ConnectFailed(TapetiConnectionParams connectionParams, Exception exception)
{ {
Console.WriteLine($"[Tapeti] Connection failed: {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" /> /// <inheritdoc cref="IControllerMessageContext" />
public class ControllerMessageContext : MessageContext, 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 /> /// <inheritdoc />

View File

@ -1,22 +1,31 @@
using System; using System;
using Tapeti.Config;
namespace Tapeti.Default namespace Tapeti.Default
{ {
/// <inheritdoc />
/// <summary>
/// Default ILogger implementation which does not log anything.
/// </summary>
public class DevNullLogger : ILogger 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 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 namespace Tapeti.Default
{ {
/// <inheritdoc />
/// <summary>
/// Default implementation of IExceptionStrategyContext.
/// </summary>
public class ExceptionStrategyContext : IExceptionStrategyContext 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; MessageContext = messageContext;
Exception = exception; Exception = exception;
} }
public IMessageContext MessageContext { get; }
public Exception Exception { get; } /// <inheritdoc />
public void SetConsumeResult(ConsumeResult consumeResult)
private HandlingResultBuilder handlingResult;
public HandlingResultBuilder HandlingResult
{ {
get => handlingResult ?? (handlingResult = new HandlingResultBuilder()); ConsumeResult = consumeResult;
set => handlingResult = value;
} }
} }
} }

View File

@ -2,11 +2,16 @@
namespace Tapeti.Default namespace Tapeti.Default
{ {
/// <inheritdoc />
/// <summary>
/// Default implementation of an exception strategy which marks the messages as Error.
/// </summary>
public class NackExceptionStrategy : IExceptionStrategy public class NackExceptionStrategy : IExceptionStrategy
{ {
/// <inheritdoc />
public void HandleException(IExceptionStrategyContext context) 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 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 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); private static readonly Regex NamespaceRegex = new Regex("^(Messaging\\.)?(?<exchange>[^\\.]+)", RegexOptions.Compiled | RegexOptions.Singleline);
/// <inheritdoc />
public string GetExchange(Type messageType) public string GetExchange(Type messageType)
{ {
if (messageType.Namespace == null) if (messageType.Namespace == null)

View File

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

View File

@ -4,11 +4,25 @@
namespace Tapeti.Default 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 public class RequeueExceptionStrategy : IExceptionStrategy
{ {
/// <inheritdoc />
public void HandleException(IExceptionStrategyContext context) 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 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 public class TypeNameRoutingKeyStrategy : IRoutingKeyStrategy
{ {
private const string SeparatorPattern = @" private const string SeparatorPattern = @"
@ -24,12 +31,17 @@ namespace Tapeti.Default
private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new ConcurrentDictionary<Type, string>(); private static readonly ConcurrentDictionary<Type, string> RoutingKeyCache = new ConcurrentDictionary<Type, string>();
/// <inheritdoc />
public string GetRoutingKey(Type messageType) public string GetRoutingKey(Type messageType)
{ {
return RoutingKeyCache.GetOrAdd(messageType, BuildRoutingKey); 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) protected virtual string BuildRoutingKey(Type messageType)
{ {
// Split PascalCase into dot-separated parts. If the class name ends in "Message" leave that out. // 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())); return string.Join(".", words.Select(s => s.ToLower()));
} }
private static List<string> SplitPascalCase(string value) private static List<string> SplitPascalCase(string value)
{ {
var split = SeparatorRegex.Split(value); var split = SeparatorRegex.Split(value);

View File

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

View File

@ -2,8 +2,13 @@
namespace Tapeti.Exceptions namespace Tapeti.Exceptions
{ {
/// <inheritdoc />
/// <summary>
/// Raised when a mandatory message has no route.
/// </summary>
public class NoRouteException : Exception public class NoRouteException : Exception
{ {
/// <inheritdoc />
public NoRouteException(string message) : base(message) { } 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 namespace Tapeti.Helpers
{ {
/// <summary>
/// Helper class for console applications.
/// </summary>
public static class ConsoleHelper 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() public static bool IsAvailable()
{ {
try try

View File

@ -4,8 +4,18 @@ using System.Threading.Tasks;
namespace Tapeti.Helpers namespace Tapeti.Helpers
{ {
/// <summary>
/// Helper class for executing the middleware pattern.
/// </summary>
public static class MiddlewareHelper 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) public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler)
{ {
var handlerIndex = middleware?.Count - 1 ?? -1; 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) public static async Task GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<Task>, Task> handle, Func<Task> lastHandler)
{ {
var handlerIndex = middleware?.Count - 1 ?? -1; var handlerIndex = middleware?.Count - 1 ?? -1;

View File

@ -3,8 +3,18 @@ using System.Threading.Tasks;
namespace Tapeti.Helpers namespace Tapeti.Helpers
{ {
/// <summary>
/// Helper methods for working with synchronous and asynchronous versions of methods.
/// </summary>
public static class TaskTypeHelper 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) public static bool IsTypeOrTaskOf(this Type type, Func<Type, bool> predicate, out bool isTaskOf, out Type actualType)
{ {
if (type == typeof(Task)) 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) public static bool IsTypeOrTaskOf(this Type type, Func<Type, bool> predicate, out bool isTaskOf)
{ {
return IsTypeOrTaskOf(type, predicate, out isTaskOf, out _); 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) public static bool IsTypeOrTaskOf(this Type type, Type compareTo, out bool isTaskOf)
{ {
return IsTypeOrTaskOf(type, t => t == compareTo, out 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="properties">Metadata included in the message</param>
/// <param name="body">The raw body of the message</param> /// <param name="body">The raw body of the message</param>
/// <returns></returns> /// <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> /// </summary>
public interface IDependencyResolver 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; 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); object Resolve(Type type);
} }
/// <inheritdoc />
/// <summary> /// <summary>
/// Allows registering controller classes into the IoC container. Also registers default implementations, /// Allows registering controller classes into the IoC container. Also registers default implementations,
/// so that the calling application may override these. /// so that the calling application may override these.
/// </summary> /// </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 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; 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; 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; 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; 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; 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); void RegisterController(Type type);
} }
} }

View File

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

View File

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

View File

@ -1,16 +1,46 @@
using System; using System;
using Tapeti.Config;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
namespace Tapeti namespace Tapeti
{ {
// This interface is deliberately specific and typed to allow for structured logging (e.g. Serilog) /// <summary>
// instead of only string-based logging without control over the output. /// 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 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 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 namespace Tapeti
{ {
/// <summary>
/// Translates message classes into routing keys.
/// </summary>
public interface IRoutingKeyStrategy public interface IRoutingKeyStrategy
{ {
/// <summary>
/// Determines the routing key for the given message class.
/// </summary>
/// <param name="messageType"></param>
/// <returns></returns>
string GetRoutingKey(Type messageType); 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 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 class TapetiAppSettingsConnectionParams : TapetiConnectionParams
{ {
public const string DefaultPrefix = "rabbitmq:"; private const string DefaultPrefix = "rabbitmq:";
public const string KeyHostname = "hostname"; private const string KeyHostname = "hostname";
public const string KeyPort = "port"; private const string KeyPort = "port";
public const string KeyVirtualHost = "virtualhost"; private const string KeyVirtualHost = "virtualhost";
public const string KeyUsername = "username"; private const string KeyUsername = "username";
public const string KeyPassword = "password"; private const string KeyPassword = "password";
public const string KeyPrefetchCount = "prefetchcount"; 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) public TapetiAppSettingsConnectionParams(string prefix = DefaultPrefix)
{ {
var keys = ConfigurationManager.AppSettings.AllKeys; var keys = ConfigurationManager.AppSettings.AllKeys;