@@ -391,6 +391,159 @@ await wrapper.CallSubOrchestratorAsync<string>(
391391 innerContext . LastSubOrchestrationVersion . Should ( ) . Be ( string . Empty ) ;
392392 }
393393
394+ [ Fact ]
395+ public async Task WaitForExternalEvent_PreCanceledToken_PreservesCancellationTokenAsync ( )
396+ {
397+ // Arrange
398+ TrackingOrchestrationContext innerContext = new ( ) ;
399+ OrchestrationInvocationContext invocationContext = new ( "Test" , new ( ) , NullLoggerFactory . Instance , null ) ;
400+ TaskOrchestrationContextWrapper wrapper = new ( innerContext , invocationContext , "input" ) ;
401+ using CancellationTokenSource source = new ( ) ;
402+ source . Cancel ( ) ;
403+
404+ // Act
405+ Task < string > wait = wrapper . WaitForExternalEvent < string > ( "Event" , source . Token ) ;
406+
407+ // Assert
408+ wait . IsCanceled . Should ( ) . BeTrue ( ) ;
409+ TaskCanceledException exception = await Assert . ThrowsAsync < TaskCanceledException > ( ( ) => wait ) ;
410+ exception . CancellationToken . Should ( ) . Be ( source . Token ) ;
411+ wrapper . CompleteExternalEvent ( "Event" , "\" payload\" " ) ;
412+ Task < string > nextWait = wrapper . WaitForExternalEvent < string > ( "Event" ) ;
413+ nextWait . IsCompletedSuccessfully . Should ( ) . BeTrue ( ) ;
414+ ( await nextWait ) . Should ( ) . Be ( "payload" ) ;
415+ }
416+
417+ [ Theory ]
418+ [ InlineData ( false ) ]
419+ [ InlineData ( true ) ]
420+ public async Task WaitForExternalEvent_BufferedEvent_CompletesSynchronouslyBeforeCancellationAsync ( bool preCanceled )
421+ {
422+ // Arrange
423+ TrackingOrchestrationContext innerContext = new ( ) ;
424+ OrchestrationInvocationContext invocationContext = new ( "Test" , new ( ) , NullLoggerFactory . Instance , null ) ;
425+ TaskOrchestrationContextWrapper wrapper = new ( innerContext , invocationContext , "input" ) ;
426+ using CancellationTokenSource source = new ( ) ;
427+ wrapper . CompleteExternalEvent ( "Event" , "\" payload\" " ) ;
428+ if ( preCanceled )
429+ {
430+ source . Cancel ( ) ;
431+ }
432+
433+ // Act
434+ Task < string > wait = wrapper . WaitForExternalEvent < string > ( "Event" , source . Token ) ;
435+ source . Cancel ( ) ;
436+
437+ // Assert
438+ wait . IsCompletedSuccessfully . Should ( ) . BeTrue ( ) ;
439+ ( await wait ) . Should ( ) . Be ( "payload" ) ;
440+ }
441+
442+ [ Fact ]
443+ public async Task CompleteExternalEvent_CancelableWait_PreservesSynchronousContinuationsAsync ( )
444+ {
445+ // Arrange
446+ TrackingOrchestrationContext innerContext = new ( ) ;
447+ OrchestrationInvocationContext invocationContext = new ( "Test" , new ( ) , NullLoggerFactory . Instance , null ) ;
448+ TaskOrchestrationContextWrapper wrapper = new ( innerContext , invocationContext , "input" ) ;
449+ using CancellationTokenSource source = new ( ) ;
450+ Task < string > wait = wrapper . WaitForExternalEvent < string > ( "Event" , source . Token ) ;
451+ Task < string > canceledWait = wrapper . WaitForExternalEvent < string > ( "OtherEvent" , source . Token ) ;
452+ bool continuationRan = false ;
453+ Task continuation = wait . ContinueWith (
454+ _ =>
455+ {
456+ continuationRan = true ;
457+ source . Cancel ( ) ;
458+ } ,
459+ CancellationToken . None ,
460+ TaskContinuationOptions . ExecuteSynchronously ,
461+ TaskScheduler . Default ) ;
462+
463+ // Act
464+ wrapper . CompleteExternalEvent ( "Event" , "\" payload\" " ) ;
465+
466+ // Assert
467+ continuationRan . Should ( ) . BeTrue ( ) ;
468+ canceledWait . IsCanceled . Should ( ) . BeTrue ( ) ;
469+ wait . IsCompletedSuccessfully . Should ( ) . BeTrue ( ) ;
470+ ( await wait ) . Should ( ) . Be ( "payload" ) ;
471+ await continuation ;
472+ }
473+
474+ [ Fact ]
475+ public async Task CompleteExternalEvent_CancelableWaiters_PreservesLifoOrderAsync ( )
476+ {
477+ // Arrange
478+ TrackingOrchestrationContext innerContext = new ( ) ;
479+ OrchestrationInvocationContext invocationContext = new ( "Test" , new ( ) , NullLoggerFactory . Instance , null ) ;
480+ TaskOrchestrationContextWrapper wrapper = new ( innerContext , invocationContext , "input" ) ;
481+ using CancellationTokenSource liveSource = new ( ) ;
482+ using CancellationTokenSource canceledSource = new ( ) ;
483+ Task < string > oldest = wrapper . WaitForExternalEvent < string > ( "Event" , liveSource . Token ) ;
484+ Task < string > canceled = wrapper . WaitForExternalEvent < string > ( "Event" , canceledSource . Token ) ;
485+ Task < string > newest = wrapper . WaitForExternalEvent < string > ( "Event" , liveSource . Token ) ;
486+ canceledSource . Cancel ( ) ;
487+
488+ // Act
489+ wrapper . CompleteExternalEvent ( "Event" , "\" first\" " ) ;
490+ wrapper . CompleteExternalEvent ( "Event" , "\" second\" " ) ;
491+ liveSource . Cancel ( ) ;
492+
493+ // Assert
494+ canceled . IsCanceled . Should ( ) . BeTrue ( ) ;
495+ ( await newest ) . Should ( ) . Be ( "first" ) ;
496+ ( await oldest ) . Should ( ) . Be ( "second" ) ;
497+ }
498+
499+ [ Fact ]
500+ public async Task CompleteExternalEvent_CancellationRace_DeliversOrBuffersExactlyOnceAsync ( )
501+ {
502+ for ( int i = 0 ; i < 100 ; i ++ )
503+ {
504+ // Arrange
505+ TrackingOrchestrationContext innerContext = new ( ) ;
506+ OrchestrationInvocationContext invocationContext = new ( "Test" , new ( ) , NullLoggerFactory . Instance , null ) ;
507+ TaskOrchestrationContextWrapper wrapper = new ( innerContext , invocationContext , "input" ) ;
508+ using CancellationTokenSource source = new ( ) ;
509+ using Barrier start = new ( 2 ) ;
510+ Task < string > wait = wrapper . WaitForExternalEvent < string > ( "Event" , source . Token ) ;
511+
512+ // Act
513+ Task cancellation = Task . Run ( ( ) =>
514+ {
515+ start . SignalAndWait ( ) ;
516+ source . Cancel ( ) ;
517+ } ) ;
518+ Task completion = Task . Run ( ( ) =>
519+ {
520+ start . SignalAndWait ( ) ;
521+ wrapper . CompleteExternalEvent ( "Event" , "\" payload\" " ) ;
522+ } ) ;
523+ await Task . WhenAll ( cancellation , completion ) . WaitAsync ( TimeSpan . FromSeconds ( 30 ) ) ;
524+
525+ // Assert
526+ if ( wait . IsCanceled )
527+ {
528+ TaskCanceledException exception = await Assert . ThrowsAsync < TaskCanceledException > ( ( ) => wait ) ;
529+ exception . CancellationToken . Should ( ) . Be ( source . Token ) ;
530+ Task < string > bufferedWait = wrapper . WaitForExternalEvent < string > ( "Event" ) ;
531+ bufferedWait . IsCompletedSuccessfully . Should ( ) . BeTrue ( ) ;
532+ ( await bufferedWait ) . Should ( ) . Be ( "payload" ) ;
533+ }
534+ else
535+ {
536+ wait . IsCompletedSuccessfully . Should ( ) . BeTrue ( ) ;
537+ ( await wait ) . Should ( ) . Be ( "payload" ) ;
538+ }
539+
540+ Task < string > nextWait = wrapper . WaitForExternalEvent < string > ( "Event" ) ;
541+ nextWait . IsCompleted . Should ( ) . BeFalse ( ) ;
542+ wrapper . CompleteExternalEvent ( "Event" , "\" next\" " ) ;
543+ ( await nextWait ) . Should ( ) . Be ( "next" ) ;
544+ }
545+ }
546+
394547 [ Fact ]
395548 public void CompleteExternalEvent_CanceledWaiter_BuffersEventForNextWaiter ( )
396549 {
0 commit comments