solidservices
solidservices copied to clipboard
decorator for async IQueryHandler
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.
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?
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>).
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.
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.