@@ -565,4 +565,77 @@ private static void ServiceContract_TypedProxy_AsyncTask_Call_TestImpl(Binding b
565565 ScenarioTestHelpers . CloseCommunicationObjects ( ( ICommunicationObject ) serviceProxy , factory ) ;
566566 }
567567 }
568+
569+ [ WcfFact ]
570+ [ OuterLoop ]
571+ public static async Task ServiceContract_TypedProxy_Task_Call_AsyncLocal_NonCapture ( )
572+ {
573+ // This test verifies a task based call to a service operation doesn't capture any AsyncLocal's in an ExecutionContext
574+ // This is indirectly checking that the OperationContext won't get captured by a registered CancellationToken callback
575+ CustomBinding customBinding = null ;
576+ ChannelFactory < IWcfServiceGenerated > factory = null ;
577+ EndpointAddress endpointAddress = null ;
578+ IWcfServiceGenerated serviceProxy = null ;
579+ string requestString = "Hello" ;
580+ string result = null ;
581+ WeakReference < OperationContextExtension > opContextReference = null ;
582+
583+ try
584+ {
585+ // *** SETUP *** \\
586+ customBinding = new CustomBinding ( ) ;
587+ customBinding . Elements . Add ( new TextMessageEncodingBindingElement ( ) ) ;
588+ customBinding . Elements . Add ( new HttpTransportBindingElement ( ) ) ;
589+ endpointAddress = new EndpointAddress ( Endpoints . DefaultCustomHttp_Address ) ;
590+ factory = new ChannelFactory < IWcfServiceGenerated > ( customBinding , endpointAddress ) ;
591+ serviceProxy = factory . CreateChannel ( ) ;
592+
593+ // *** EXECUTE *** \\
594+ var opExtension = new OperationContextExtension ( ) ;
595+ opContextReference = new WeakReference < OperationContextExtension > ( opExtension ) ;
596+ using ( new OperationContextScope ( ( IContextChannel ) serviceProxy ) )
597+ {
598+ OperationContext . Current . Extensions . Add ( opExtension ) ;
599+ result = await serviceProxy . EchoAsync ( requestString ) ;
600+ }
601+
602+ opExtension = null ;
603+
604+ // The Task generated by the compiler for this method stores the current ExecutionContext when an await is hit.
605+ // This means even after we've nulled out the OperationContext via disposing the OperationContextScope, it's still
606+ // stored in the saved ExecutionContext which was captured by the Task. Forcing another async continuation via
607+ // Task.Yield() causes the stored ExecutionContext to be replace with the current one which doesn't reference
608+ // OperationContext any more. Without this, the OperationContext would be kept alive until the next await call,
609+ // and the weak reference would still hold a reference to the OperationContextExtension.
610+ await Task . Yield ( ) ;
611+
612+ // Force a Gen2 GC so that the WeakReference will no longer have the OperationContextExtension as a target.
613+ GC . Collect ( 2 ) ;
614+
615+ // *** VALIDATE *** \\
616+ Assert . Equal ( requestString , result ) ;
617+ Assert . False ( opContextReference . TryGetTarget ( out OperationContextExtension opContext ) , "OperationContextExtension should have been collected" ) ;
618+ // *** CLEANUP *** \\
619+ factory . Close ( ) ;
620+ ( ( ICommunicationObject ) serviceProxy ) . Close ( ) ;
621+ }
622+ finally
623+ {
624+ // *** ENSURE CLEANUP *** \\
625+ ScenarioTestHelpers . CloseCommunicationObjects ( ( ICommunicationObject ) serviceProxy , factory ) ;
626+ }
627+ }
628+
629+ public class OperationContextExtension : IExtension < OperationContext >
630+ {
631+ public void Attach ( OperationContext owner )
632+ {
633+ // Do nothing
634+ }
635+ public void Detach ( OperationContext owner )
636+ {
637+ // Do nothing
638+ }
639+ }
640+
568641}
0 commit comments