Skip to content

Commit 509219a

Browse files
committed
Test to validate UnsafeRegister fixes holding a reference to OperationContextScope
1 parent a47960f commit 509219a

File tree

1 file changed

+73
-0
lines changed

1 file changed

+73
-0
lines changed

src/System.Private.ServiceModel/tests/Scenarios/Client/TypedClient/TypedProxyTests.4.0.0.cs

+73
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)