As I understand it, you're essentially restarting a watchdog timer for three seconds every time you click a different email. If the WDT expires without getting a new click then you go ahead and mark the latest email as 'Read'.
For years, I used (successfully) this same approach to cancel the old task when starting a new one for the WDT. But then I realized I could just do this:
int _wdtCount = 0;
// Returning 'void' to emphasize that this should 'not' be awaited!
async void onEmailClicked(int id)
{
_wdtCount++;
var capturedCount = _wdtCount;
await Task.Delay(TimeSpan.FromSeconds(3));
// If the 'captured' localCount has not changed after waiting 3 seconds
// it indicates that no new selections have been made in that time.
if(capturedCount.Equals(_wdtCount))
{
await MarkEmailAsRead(id);
}
}
async Task MarkEmailAsRead(int id)
{
Console.WriteLine($"The email with the ID '{id}' has been marked read");
}
Anyway, works for me and it's just a thought...
TESTBENCH
#region T E S T
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T

EDIT
Though I realize that the post no longer has the wording entertaining 'other' options than task cancellation, I still wanted to incorporate the excellent comments and improve my original answer by putting the WatchDogTimer
in a class...
class WatchDogTimer
{
int _wdtCount = 0;
public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);
public void ThrowBone(Action action)
{
_wdtCount++;
var capturedCount = _wdtCount;
Task
.Delay(Interval)
.GetAwaiter()
.OnCompleted(() =>
{
// If the 'captured' localCount has not changed after
// awaiting the Interval, it indicates that no new
// 'bones' have been thrown during that interval.
if (capturedCount.Equals(_wdtCount))
{
action();
}
});
}
}
... and eliminating the async void
from the posit.
void onEmailClicked(int id)
=> _watchDog.ThrowBone(() => MarkEmailAsRead(id));
void MarkEmailAsRead(int id)
{
// BeginInvoke(()=>
// {
Console.WriteLine($"Expired: {_stopWatch.Elapsed}");
Console.WriteLine($"The email with the ID '{id}' has been marked read");
// });
}
TESTBENCH
#region T E S T
// Same test parameters as before.
System.Diagnostics.Stopwatch _stopWatch =
new System.Diagnostics.Stopwatch();
WatchDogTimer _watchDog =
new WatchDogTimer { Interval = TimeSpan.FromSeconds(3) };
_stopWatch.Start();
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T
