jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
10.91k stars 1.16k forks source link

IRequestPostProcessor does not run after IRequestExceptionHandler #932

Closed VictorioBerra closed 8 months ago

VictorioBerra commented 1 year ago

Repro:

https://dotnetfiddle.net/AvoV1L

using System;
using MediatR;
using MediatR.Pipeline;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Ardalis.Result;
using System.Collections.Generic;

public class Program
{
    static async Task Main()
    {
        var services = new ServiceCollection();

        services.AddMediatR(cfg =>
        {
            cfg.RegisterServicesFromAssemblies(typeof(EnableLdapAccountCommand).Assembly);
            cfg.AddOpenRequestPostProcessor(typeof(StepRecorderingRequestPostProcessor<,>));
        });

        services.AddTransient<IRequestHandler<EnableLdapAccountCommand, Result>, EnableLdapAccountHandler>();
        services.AddTransient<IRequestHandler<DisableLdapAccountCommand, Result>, DisableLdapAccountHandler>();

        var provider = services.BuildServiceProvider();

        var mediator = provider.GetRequiredService<IMediator>();

        var correlationId = Guid.NewGuid().ToString();
        List<IWorkflowCommand> steps = new List<IWorkflowCommand>();
        steps.Add(new EnableLdapAccountCommand
        {
            SAMAccountName = "EnableMe",
            CorrelationId = correlationId,
        });     
        steps.Add(new DisableLdapAccountCommand
        {
            SAMAccountName = "DisableMe",
            CorrelationId = correlationId,
        });     

        var results = new List<IResult>();
        foreach (var step in steps)
        {
            try
            {
                var response = await mediator.Send(step);
                results.Add((IResult)response);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Global unhandled exception. {e.Message}");
            }
        }

        results.Dump();
    }
}

public interface IWorkflowCommand
{
    public string CorrelationId { get; set; }
}

public class DisableLdapAccountCommand : IRequest<Result>, IWorkflowCommand
{
    public string SAMAccountName { get; set; }

    public string CorrelationId { get; set; }
}

public class EnableLdapAccountCommand : IRequest<Result>, IWorkflowCommand
{
    public string SAMAccountName { get; set; }

    public string CorrelationId { get; set; }
}

public class EnableLdapAccountHandler : IRequestHandler<EnableLdapAccountCommand, Result>
{
    public Task<Result> Handle(EnableLdapAccountCommand request, CancellationToken cancellationToken)
    {
        throw new Exception("EnableLdapAccountCommand Exception");
    }
}

public class DisableLdapAccountHandler : IRequestHandler<DisableLdapAccountCommand, Result>
{
    public Task<Result> Handle(DisableLdapAccountCommand request, CancellationToken cancellationToken)
    {
        return Task.FromResult(Result.Success());
    }
}

public class StepRecorderingRequestPostProcessor<TRequest, TResponse> : IRequestPostProcessor<TRequest, TResponse>
    where TRequest : IWorkflowCommand
    where TResponse : Result
{
    public Task Process(TRequest request, TResponse response, CancellationToken cancellationToken)
    {
        $"Post-Ran: ({request.GetType().Name}) {response.Status}".Dump();
        return Task.CompletedTask;
    }
}

public class GenericRequestExceptionHandler<TRequest, TResponse, TException> : IRequestExceptionHandler<TRequest, TResponse, TException>
    where TRequest : IWorkflowCommand
    where TResponse : Result
    where TException : Exception
{
    public Task Handle(
        TRequest request,
        TException exception,
        RequestExceptionHandlerState<TResponse> state,
        CancellationToken cancellationToken = new CancellationToken())
    {
        Console.WriteLine("GenericRequestExceptionHandler translating exception to error.");
        var translatedResponse = Result.Error("Unhandled exception.");
        state.SetHandled((TResponse)translatedResponse); // ??? What is this error? There is a constraint that says TResponse is type of Result. So why does this mnot work?!?
        return Task.CompletedTask;
    }
}

For me this is important because I would expect my post processor to wrap up auditing, close transactions, etc. This does not happen even though the exception is explicitly handled and a result is returned.

VictorioBerra commented 1 year ago

I think I am way out of my depth here. This fixes it, and I have no idea why:

services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblies(this.GetType().Assembly);
    cfg.AddOpenRequestPostProcessor(typeof(StepRecorderingRequestPostProcessor<,>));
    cfg.AddOpenBehavior(typeof(KeepGoinRequestExceptionProcessorBehavior<,>));
});

public class KeepGoinRequestExceptionProcessorBehavior<TRequest, TResponse> : RequestExceptionProcessorBehavior<TRequest, TResponse>
{
    public KeepGoinRequestExceptionProcessorBehavior(IServiceProvider serviceProvider)
        : base(serviceProvider)
    {

    }

    public async Task<TResponse> HandleAsync(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        throw new Exception("Here we go");
    }
}

Working repro: https://dotnetfiddle.net/WWjl80

VictorioBerra commented 1 year ago

@jbogard any ideas on this one?

jbogard commented 11 months ago

I'm not sure what's "correct" here. But you can always create your own behaviors that wrap the exception behavior. Unfortunately there's no right or wrong here, other than adding more pre/post processors. "Pre/PostException" etc.

github-actions[bot] commented 9 months ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] commented 8 months ago

This issue was closed because it has been stalled for 14 days with no activity.