solidservices icon indicating copy to clipboard operation
solidservices copied to clipboard

decorator for async IQueryHandler

Open ndc opened this issue 1 year ago • 4 comments

I want to create a decorator for my IQueryHandler that returns a Task<Something>. With the standard decorator I can add some synchronous actions before calling the decorated even if the result is a Task<Something>:

public interface IQuery<TResult> { }

public interface IQueryHandler<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

public class ExampleDecorator<TQuery, TResult>(
    IQueryHandler<TQuery, TResult> decorated
    ) : IQueryHandler<TQuery, TResult> where TQuery : ICommand<TResult>
{
    public TResult Handle(TQuery query)
    {
        // do something synchronous

        var result = decorated.Handle(query);
        return result;  // returning Task<Something>
    }
}

But if I need to do something after calling the decorated, what is the best way to do it? Should I try to determine the type of the result variable, if it is Task then get the generic type, then call Task.Result? Or should I create IQueryHandlerAsync? I tried to do this but couldn't get the syntax right.

ndc avatar Aug 22 '24 13:08 ndc

Of course I figured the async syntax after posting :)

public interface IQueryHandlerAsync<TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query);
}

public class ExampleDecorator<TQuery, TResult>(
    IQueryHandlerAsync<TQuery, TResult> decorated
    ) : IQueryHandlerAsync<TQuery, TResult> where TQuery : ICommand<TResult>
{
    public async Task<TResult> HandleAsync(TQuery query)
    {
        await SomethingBeforeDecorated();

        var result = await decorated.HandleAsync(query);

        await SomethingAfterDecorated();

        return result;
    }
}

But is this the best way? The sync and async flow must use different interfaces?

ndc avatar Aug 22 '24 13:08 ndc

The "best" is always subjective, but... if you can, choose one pattern for the complete application, meaning: Either you make everything synchronous or you make everything asynchronous. Having two models can complicate things quite significantly. Because of the nature of .NET a.t.m. I would choose for the asynchronous model, and thus making both the IQueryHandler and ICommandHandler abstractions purely asynchronous (both returning Task<T>).

dotnetjunkie avatar Aug 23 '24 09:08 dotnetjunkie

Thanks for the suggestion! Yes I can imagine if both are available the caller will be confused which one to use and most likely only one is implemented. And with async one it can be made somewhat synchronous with Task.FromResult, while the synchronous one can't handle internal async calls.

ndc avatar Aug 24 '24 04:08 ndc

You can have both synchronous and asynchronous interfaces, while having an asynchronous caller. The trick is to have an async-to-sync adapter implementation, such that the caller has no idea that he is using a synchronous implementation under the covers.

Such solution, however, would lead to extra complication because all synchronous implementations would need to be wrapped by the async-to-sync adapter while the async implementations will be used directly. Although you can do this with a few lines of code in Simple Injector, it does mean you'll have an extra layer of indirection and a duplicate set of abstractions to work with. In most cases, such extra level of obfuscation is not worth the trouble.

As you suggested above, the simpler thing to do will be to have your synchronous implementations just return Task.FromResult or Task.CompletedTask.

dotnetjunkie avatar Aug 24 '24 07:08 dotnetjunkie