simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 155 forks source link

How to register a decorator for commands with a specific return type #973

Closed stevenrobertscrew closed 8 months ago

stevenrobertscrew commented 1 year ago

hello,

I need to decorate a command handler but only for commands that have a specific return type. In this case my command has a return type, mainly just the id that was created/updated or whatever.

public interface ICommand { }
public interface ICommand<TResult> : ICommand { }

public interface IHandleCommandAsync<in TCommand, TResult> where TCommand : ICommand<TResult>
{
    Task<IOpResult<TResult>> HandleAsync(TCommand command);
}

//actual command object
public class CreateReservationByUser : ICommand<Id>
{
    public CreateReservationByUser(int reservationId, string paymentToken)
    {
        ReservationId = reservationId;
        PaymentToken = paymentToken;
    }

    public int ReservationId { get; }
    public string PaymentToken { get; }
}

public record struct Id(int ReservationId)
{
    public static implicit operator Id(int d) => new(d);
};

I have a decorator that I only wish to apply when TResult is a specific type; I need to do something with that specific type like log it. Here's an example of that decorator

public class ReservationAuditStoreDecorator : IHandleCommandAsync<CreateReservationByUser, Id>
{
    private readonly IReservationAuditStore _reservationAuditStore;
    private readonly IHandleCommandAsync<CreateReservationByUser, Id> _decorated;

    public ReservationAuditStoreDecorator(IHandleCommandAsync<CreateReservationByUser, Id> decorated,
        IReservationAuditStore reservationAuditStore)
    {
        _reservationAuditStore = reservationAuditStore;
        _decorated = decorated;
    }

    public async Task<IOpResult<Id>> HandleAsync(CreateReservationByUser command)
    {
        var result = await _decorated.HandleAsync(command);
        if (result.IsFailure)
        {
            return result;
        }

        var auditStoreResult = await _reservationAuditStore.AppendAsync(result.Value.ReservationId, command);
        return auditStoreResult.IsFailure
            ? OpResult.Fail<Id>(auditStoreResult.Exception)
            : result;
    }
}

This works when I register like this

container.RegisterDecorator<IHandleCommandAsync<CreateReservationByUser, Id>, ReservationAuditStoreDecorator>(
    Lifestyle.Scoped);

When this is the only use case I don't mind, but now along comes a secondary command that needs the same log. Both return types have an Id property that I need to log. I tried creating an interface to make this clearer like this

public interface IReservationCommandWithId : ICommand<Id>
{
}

and registering the decorator like this

container.RegisterDecorator<IHandleCommandAsync<IReservationCommandWithId, Id>, ReservationAuditStoreDecorator>(
    Lifestyle.Scoped);

For reasons that will no doubt be obvious to you, this compiles and runs but ReservationAuditStoreDecorator is never decorating the handler. I'm sure I'm missing something just silly.

I have considered using the predicate way of registering the decorator and then inside the decorator casting the result to Id but that means I'm depending on the registration predicate when I'd rather depend on type safety. Please can you help?

dotnetjunkie commented 1 year ago

Change your decorator to the following:

public class ReservationAuditStoreDecorator<TCommand>
    : IHandleCommandAsync<TCommand, Id>
{
    private readonly IReservationAuditStore _reservationAuditStore;
    private readonly IHandleCommandAsync<TCommand, Id> _decorated;

    public ReservationAuditStoreDecorator(
        IHandleCommandAsync<TCommand, Id> decorated,
        IReservationAuditStore reservationAuditStore)
    {
        _reservationAuditStore = reservationAuditStore;
        _decorated = decorated;
    }

    public async Task<IOpResult<Id>> HandleAsync(TCommand command)
    {
        var result = await _decorated.HandleAsync(command);

        if (result.IsFailure)
        {
            return result;
        }

        var auditStoreResult = await _reservationAuditStore.AppendAsync(
            result.Value.ReservationId, command);

        return auditStoreResult.IsFailure
            ? OpResult.Fail<Id>(auditStoreResult.Exception)
            : result;
    }
}

And register it as follows:

container.RegisterDecorator(
    typeof(IHandleCommandAsync<,>),
    typeof(ReservationAuditStoreDecorator<>),
    Lifestyle.Scoped);

In other words, you make the decorator generic, giving it a TCommand generic type argument that maps to the TCommand argument of the ICommandHandlerAsync<TCommand, TResult> interface, where the TResult is filled in with Id. Of course every part of the code that refers to CreateReservationByUser needs to be replaced by TCommand. This decorator can be registered as shown above and Simple Injector will do the rest.

stevenrobertscrew commented 1 year ago

ugh. I guess I missed a spot. I separately have a decorator for the 'normal' commands (e.g. ones without a return type). Just using different names would solve for that... DUH! Thanks @dotnetjunkie