Search code examples
.net-corerebus

Is there a way to create a generic IFailed<T> to catch unimplemented second level retries in Rebus?


Is there a way to create and register a generic IFailed that would catch second level retries that have no handlers?

We use second-level retries enabled and have our handlers implementing IHandleMessages<SomeCommand> as well as IHandleMessages<IFailed<SomeCommand>>.

However, not all our handlers are implementing the IFailed<SomeCommand> interface and this is causing some exceptions to bubble up multiple times when SomeCommand fails.

Is there a way to register a generic IHandleMessages<IFailed<T>> that will handle all failed commands that have not been handled properly?

I'm thinking of at least logging T has failed and we will not attempt second level retries or something similar if the command has failed.


Solution

  • Yes there is 🙂

    To get to handle all failed messages, you can simply register a handler that implements IHandleMessages<IFailed<object>>.

    If you want it to be a "fallback" (i.e. only be used when there's no explicit handler for a given failed messages type), then you can decorate Rebus' IHandlerActivator and filter the returned handlers.

    Here's how:

    Create an extension method with a handler activator decorator

    static class FallbackMessageHandlerExtensions
    {
        /// <summary>
        /// Marks handlers of type <typeparamref name="THandler"/> as a "fallback handler", which means that it will
        /// only be used in cases where there are no other handlers.
        /// </summary>
        public static void MarkAsFallbackHandler<THandler>(this OptionsConfigurer configurer) where THandler : IHandleMessages
        {
            if (configurer == null) throw new ArgumentNullException(nameof(configurer));
    
            configurer.Decorate<IHandlerActivator>(c => new HandlerInvokerRemover<THandler>(c.Get<IHandlerActivator>()));
        }
    
        class HandlerInvokerRemover<THandlerType> : IHandlerActivator
        {
            readonly IHandlerActivator _handlerActivator;
    
            public HandlerInvokerRemover(IHandlerActivator handlerActivator)
            {
                _handlerActivator = handlerActivator ?? throw new ArgumentNullException(nameof(handlerActivator));
            }
    
            public async Task<IEnumerable<IHandleMessages<TMessage>>> GetHandlers<TMessage>(TMessage message, ITransactionContext transactionContext)
            {
                var handlers = await _handlerActivator.GetHandlers(message, transactionContext);
                var handlersList = handlers.ToList();
    
                // if there's more than one handler, there's potential for having included the 
                // fallback handler without having the need for a fallback
                if (handlersList.Count > 1)
                {
                    handlersList.RemoveAll(h => h is THandlerType);
                }
    
                return handlersList;
            }
        }
    }
    

    and then ensure that second level retries and your fallback mechanism are enabled

    services.AddRebus(
        configure => configure
            .(...)
            .Options(o =>
            {
                o.RetryStrategy(secondLevelRetriesEnabled: true);
                o.MarkAsFallbackHandler<FallbackFailedMessageHandler>();
            })
    );
    
    

    You can see a full example here: https://github.com/rebus-org/Rebus.ServiceProvider/blob/master/Rebus.ServiceProvider.Tests/Examples/AddMessageHandlerFallback.cs