My recommendation is to just use TPL Dataflow and be done with it. The more you abuse a language feature like async
(or IEnumerable
for that matter), the less maintainable your code is.
That said...
You do (sort of) have some hooks around await
: custom awaitables. Stephen Toub has a blog on them and Jon Skeet covers some details in his eduasync series.
However, you cannot return a custom awaitable from an async
method, so this approach would mean all your await
s would have to "opt-in" to the "check" behavior:
async Task DoSomething()
{
var someObject = SomeService.Get(...);
using (var check = new SanityCheckScope())
{
check.StopWhen(() => someObject.Lifetime < 0);
await SomeOtherStuff().WithChecks();
}
}
It's not clear to me what the semantics should be when a check fails. From your question, it sounds like once a check fails you just want that method to abort, so the check is more like a code contract than a monitor or condition variable. It also sounds like the scope should only be applied to that method and not methods called from that method.
You may be able to get something working by using a custom awaiter which encapsulates the returned Task
and uses TaskCompletionSource<T>
to expose a different Task
. The custom awaiter would perform the check before executing the continuation of the async
method.
Another thing to watch out for is how your "scope" works. Consider this situation:
async Task DoSomething()
{
using (var check = new SanityCheckScope())
{
check.StopWhen(...);
await SomeOtherStuff();
await MoreOtherStuff();
}
}
Or even more fun:
async Task DoSomething()
{
using (var check = new SanityCheckScope())
{
check.StopWhen(...);
await SomeOtherStuff();
await Task.WhenAll(MoreOtherStuff(), MoreOtherStuff());
}
}
I suspect your scope will have to make use of AsyncLocal<T>
(described on my blog) in order to work properly.