From fbbbf057d411f179a792535e540516f782ca6322 Mon Sep 17 00:00:00 2001 From: Matt Connew Date: Fri, 2 Apr 2021 14:50:08 -0700 Subject: [PATCH] Add support for OperationContext/OperationContextScope flow through an await Fixed the ability to run tests in Visual Studio --- .gitignore | 4 + Directory.Build.props | 6 + Directory.Build.targets | 1 + eng/testing/.runsettings | 49 +++++ eng/testing/runsettings.targets | 55 +++++ .../ServiceModel/Dispatcher/MessageRpc.cs | 9 +- .../System/ServiceModel/OperationContext.cs | 37 +++- .../ServiceModel/OperationContextScope.cs | 36 +++- .../Infrastructure/ServiceUtilHelper.cs | 9 + .../Infrastructure/xunit/WcfTestCase.cs | 57 +++--- .../Infrastructure/xunit/WcfTestDiscoverer.cs | 15 +- .../ThreadHoppingSynchronizationContext.cs | 31 +++ .../Http/Binding.Http.IntegrationTests.csproj | 2 +- .../Http/ModuleInitializerAttribute.cs | 12 ++ .../Binding/Http/OperationContextTests.cs | 63 ++++++ .../Binding/Tcp/NetTcpBindingTests.4.0.0.cs | 2 +- .../Binding/Tcp/NetTcpBindingTests.4.1.0.cs | 3 +- .../Tcp/OperationContextAsyncFlowTests.cs | 192 ++++++++++++++++++ 18 files changed, 528 insertions(+), 55 deletions(-) create mode 100644 eng/testing/.runsettings create mode 100644 eng/testing/runsettings.targets create mode 100644 src/System.Private.ServiceModel/tests/Common/Scenarios/ThreadHoppingSynchronizationContext.cs create mode 100644 src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/ModuleInitializerAttribute.cs create mode 100644 src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/OperationContextTests.cs create mode 100644 src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/OperationContextAsyncFlowTests.cs diff --git a/.gitignore b/.gitignore index a1a70e8bfb9..a79f8547f45 100644 --- a/.gitignore +++ b/.gitignore @@ -276,3 +276,7 @@ Session.vim # Private test configuration and binaries. config.ps1 **/IISApplications + +# Debug files +*.etl +*.dmp \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 118a52d4d57..5163325079d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,12 @@ netcoreapp3.1 + + + true + + + diff --git a/Directory.Build.targets b/Directory.Build.targets index ff17bd1407b..0b4e011fc65 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,4 +4,5 @@ + diff --git a/eng/testing/.runsettings b/eng/testing/.runsettings new file mode 100644 index 00000000000..ae33caf1711 --- /dev/null +++ b/eng/testing/.runsettings @@ -0,0 +1,49 @@ + + + + + 300000 + + .\TestResults\ + + .\ + + $$MAXCPUCOUNT$$ + + $$DISABLEPARALLELIZATION$$ + + $$DISABLEAPPDOMAIN$$ + + $$TESTCASEFILTER$$ + $$DOTNETHOSTPATH$$ + FrameworkCore10 + + + + + + + + Minimal + + + + + + + + + + + + diff --git a/eng/testing/runsettings.targets b/eng/testing/runsettings.targets new file mode 100644 index 00000000000..66ccef4dd32 --- /dev/null +++ b/eng/testing/runsettings.targets @@ -0,0 +1,55 @@ + + + $(MSBuildThisFileDirectory).runsettings + $(ArtifactsObjDir)$(TargetOS)-$(Configuration)-$(TargetArchitecture).runsettings + $(OutDir).runsettings + + false + $(RunSettingsIntermediateOutputFilePath) + $(RunSettingsAppOutputFilePath) + + + $(RunSettingsAppOutputFilePath) + + $(RunSettingsIntermediateOutputFilePath) + + GenerateRunSettingsFile;$(PrepareForRunDependsOn) + + + + <_testFilter Condition="'$(_withCategories)' != ''">$(_withCategories.Replace(';', '&amp;category=')) + <_testFilter Condition="'$(_withoutCategories)' != ''">$(_testFilter)$(_withoutCategories.Replace(';', '&amp;category!=')) + <_testFilter>$(_testFilter.Trim('&amp;')) + + + + + $([System.IO.File]::ReadAllText('$(RunSettingsInputFilePath)')) + $(RunSettingsFileContent.Replace('$$MAXCPUCOUNT$$', '1')) + $(RunSettingsFileContent.Replace('$$MAXCPUCOUNT$$', '0')) + + + $(RunSettingsFileContent.Replace('$$COVERAGE_INCLUDE$$', '$(CoverageIncludeFilter)') + .Replace('$$COVERAGE_EXCLUDEBYFILE$$', '$(CoverageExcludeByFileFilter)') + .Replace('$$COVERAGE_INCLUDEDIRECTORY$$', '$(CoverageIncludeDirectoryFilter)') + .Replace('$$COVERAGE_ENABLED$$', '$([MSBuild]::ValueOrDefault('$(Coverage)', 'false'))') + .Replace('$$DISABLEPARALLELIZATION$$', '$([MSBuild]::ValueOrDefault('$(TestDisableParallelization)', 'false'))') + .Replace('$$DISABLEAPPDOMAIN$$', '$([MSBuild]::ValueOrDefault('$(TestDisableAppDomain)', 'false'))') + .Replace('$$TESTCASEFILTER$$', '$(_testFilter)') + .Replace('$$DOTNETHOSTPATH$$', '$(TestHostRootPath)$([System.IO.Path]::GetFileName('$(DotNetTool)'))')) + + + + + + + $(RunSettingsOutputFilePath) + + + diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/MessageRpc.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/MessageRpc.cs index a3dd9e9c76b..63f66332e88 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/MessageRpc.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/MessageRpc.cs @@ -409,15 +409,12 @@ internal bool Process(bool isOperationContextSet) NextProcessor = null; OperationContext originalContext; - OperationContext.Holder contextHolder; if (!isOperationContextSet) { - contextHolder = OperationContext.CurrentHolder; - originalContext = contextHolder.Context; + originalContext = OperationContext.Current; } else { - contextHolder = null; originalContext = null; } IncrementBusyCount(); @@ -426,7 +423,7 @@ internal bool Process(bool isOperationContextSet) { if (!isOperationContextSet) { - contextHolder.Context = OperationContext; + OperationContext.Current = OperationContext; } processor(ref this); @@ -455,7 +452,7 @@ internal bool Process(bool isOperationContextSet) if (!isOperationContextSet) { - contextHolder.Context = originalContext; + OperationContext.Current = originalContext; } completed = !IsPaused; diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContext.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContext.cs index 14792b76ec5..a99753da74b 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContext.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContext.cs @@ -8,13 +8,24 @@ using System.Security.Principal; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; +using System.Threading; namespace System.ServiceModel { public sealed class OperationContext : IExtensibleObject { + static OperationContext() + { + DisableAsyncFlow = AppContext.TryGetSwitch("System.ServiceModel.OperationContext.DisableAsyncFlow", out var enabled) && enabled; + if (!DisableAsyncFlow) + { + s_asyncContext = new AsyncLocal(); + } + } + [ThreadStatic] private static Holder s_currentContext; + private static AsyncLocal s_asyncContext; private Message _clientReply; private bool _closeClientReply; private ExtensionCollection _extensions; @@ -80,12 +91,26 @@ public static OperationContext Current { get { - return CurrentHolder.Context; + if (DisableAsyncFlow) + { + return CurrentHolder.Context; + } + else + { + return s_asyncContext.Value; + } } set { - CurrentHolder.Context = value; + if (DisableAsyncFlow) + { + CurrentHolder.Context = value; + } + else + { + s_asyncContext.Value = value; + } } } @@ -93,16 +118,20 @@ internal static Holder CurrentHolder { get { - Holder holder = OperationContext.s_currentContext; + Holder holder; + holder = s_currentContext; if (holder == null) { holder = new Holder(); - OperationContext.s_currentContext = holder; + s_currentContext = holder; } + return holder; } } + internal static bool DisableAsyncFlow { get; } + public EndpointDispatcher EndpointDispatcher { get diff --git a/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContextScope.cs b/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContextScope.cs index 9566135310d..39bbe2ffcec 100644 --- a/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContextScope.cs +++ b/src/System.Private.ServiceModel/src/System/ServiceModel/OperationContextScope.cs @@ -2,19 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. - +using System.Threading; namespace System.ServiceModel { public sealed class OperationContextScope : IDisposable { + static OperationContextScope() + { + if (!OperationContext.DisableAsyncFlow) + { + s_asyncCurrentScope = new AsyncLocal(); + } + } + [ThreadStatic] private static OperationContextScope s_currentScope; - + private static AsyncLocal s_asyncCurrentScope; private OperationContext _currentContext; private bool _disposed; private readonly OperationContext _originalContext = OperationContext.Current; - private readonly OperationContextScope _originalScope = OperationContextScope.s_currentScope; + private readonly OperationContextScope _originalScope = OperationContext.DisableAsyncFlow ? s_currentScope : s_asyncCurrentScope.Value; public OperationContextScope(IContextChannel channel) { @@ -38,13 +46,21 @@ public void Dispose() private void PushContext(OperationContext context) { _currentContext = context; - OperationContextScope.s_currentScope = this; + if (OperationContext.DisableAsyncFlow) + { + s_currentScope = this; + } + else + { + s_asyncCurrentScope.Value = this; + } + OperationContext.Current = _currentContext; } private void PopContext() { - if (OperationContextScope.s_currentScope != this) + if ((OperationContext.DisableAsyncFlow ? s_currentScope : s_asyncCurrentScope.Value) != this) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.SFxInterleavedContextScopes0)); } @@ -54,7 +70,15 @@ private void PopContext() throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.SFxContextModifiedInsideScope0)); } - OperationContextScope.s_currentScope = _originalScope; + if (OperationContext.DisableAsyncFlow) + { + s_currentScope = _originalScope; + } + else + { + s_asyncCurrentScope.Value = _originalScope; + } + OperationContext.Current = _originalContext; if (_currentContext != null) diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ServiceUtilHelper.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ServiceUtilHelper.cs index b8154766845..43a9beb5608 100644 --- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/ServiceUtilHelper.cs +++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/ServiceUtilHelper.cs @@ -43,6 +43,9 @@ public static X509Certificate2 RootCertificate { get { + // When running under VS, the Condition checks are run in a different process than the test run + // which means the test running process won't have the certificate installed by the condition check code. + EnsureRootCertificateInstalled(); ThrowIfRootCertificateInstallationError(); return s_rootCertificate; } @@ -52,6 +55,9 @@ public static X509Certificate2 ClientCertificate { get { + // When running under VS, the Condition checks are run in a different process than the test run + // which means the test running process won't have the certificate installed by the condition check code. + EnsureClientCertificateInstalled(); ThrowIfClientCertificateInstallationError(); return s_clientCertificate; } @@ -61,6 +67,9 @@ public static X509Certificate2 PeerCertificate { get { + // When running under VS, the Condition checks are run in a different process than the test run + // which means the test running process won't have the certificate installed by the condition check code. + EnsurePeerCertificateInstalled(); ThrowIfPeerCertificateInstallationError(); return s_peerCertificate; } diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestCase.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestCase.cs index d07e418f80c..a200de00d68 100644 --- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestCase.cs +++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestCase.cs @@ -10,57 +10,38 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Xunit; using Xunit.Abstractions; using Xunit.Sdk; namespace Infrastructure.Common { /// Provides for custom IXunitTestCase. - internal class WcfTestCase : LongLivedMarshalByRefObject, IXunitTestCase + internal class WcfTestCase : XunitTestCase, IXunitTestCase { - private readonly IXunitTestCase _testCase; - private readonly string _skippedReason; - private readonly bool _isTheory; + private string _skippedReason; + private bool _isTheory; private readonly IMessageSink _diagnosticMessageSink; static TestEventListener s_testListener = new TestEventListener(new List() { "Microsoft-Windows-Application Server-Applications" }, EventLevel.Verbose); + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] public WcfTestCase() { } - internal WcfTestCase(IXunitTestCase testCase, string skippedReason = null, bool isTheory = false, IMessageSink diagnosticMessageSink = null) + internal WcfTestCase(XunitTestCase testCase, + TestMethodDisplay defaultMethodDisplay, + string skippedReason = null, + bool isTheory = false, + IMessageSink diagnosticMessageSink = null) + : base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testCase.TestMethod, testCase.TestMethodArguments) { - _testCase = testCase; _skippedReason = skippedReason; _isTheory = isTheory; _diagnosticMessageSink = diagnosticMessageSink; } - public string DisplayName { get { return _testCase.DisplayName; } } - - public IMethodInfo Method { get { return _testCase.Method; } } - - public string SkipReason { get { return _skippedReason; } } - - public ISourceInformation SourceInformation { get { return _testCase.SourceInformation; } set { _testCase.SourceInformation = value; } } - - public ITestMethod TestMethod { get { return _testCase.TestMethod; } } - - public object[] TestMethodArguments { get { return _testCase.TestMethodArguments; } } - - public Dictionary> Traits { get { return _testCase.Traits; } } - - public string UniqueID { get { return _testCase?.UniqueID; } } - - public Exception InitializationException { get { return null; } } - - public int Timeout { get { return 0; } } - - public void Deserialize(IXunitSerializationInfo info) { _testCase?.Deserialize(info); } - - public async Task RunAsync( + public override async Task RunAsync( IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) { @@ -81,7 +62,7 @@ public async Task RunAsync( { etwOutput.AppendLine(string.Format(DisplayName + ": " + item.Message, item.Payload.ToArray())); } - //The mumber of parameters in Payload does not match the number of arguments in the item.Message and thus cause a + // The mumber of parameters in Payload does not match the number of arguments in the item.Message and thus cause a // FormatException occationally, In this case, we catch and output all items in the payload and the Message without formatting the message. // https://github.com/dotnet/wcf/issues/1440 is opened to investigate the root cause of the mismatch exception. catch (FormatException e) @@ -107,6 +88,18 @@ public async Task RunAsync( return runsummary; } - public void Serialize(IXunitSerializationInfo info) { _testCase.Serialize(info); } + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + data.AddValue("_isTheory", _isTheory); + data.AddValue("_skippedReason", _skippedReason); + } + + public override void Deserialize(IXunitSerializationInfo data) + { + _isTheory = data.GetValue("_isTheory"); + _skippedReason = data.GetValue("_skippedReason"); + base.Deserialize(data); + } } } diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestDiscoverer.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestDiscoverer.cs index 2e0565fbe77..4b9a73f9213 100644 --- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestDiscoverer.cs +++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/xunit/WcfTestDiscoverer.cs @@ -45,7 +45,11 @@ internal static IEnumerable Discover( if (issueSkipList.Count > 0) { string skippedReason = string.Format("Active issue(s): {0}", string.Join(", ", issueSkipList)); - return testCases.Select(tc => new WcfTestCase(tc, skippedReason)); + return testCases.Select(tc => new WcfTestCase((XunitTestCase)tc, + discoveryOptions.MethodDisplayOrDefault(), + skippedReason, + isTheory, + diagnosticMessageSink)); } } @@ -69,14 +73,19 @@ internal static IEnumerable Discover( if (skipReasons.Count > 0) { string skippedReason = string.Format("Condition(s) not met: {0}", string.Join(", ", skipReasons)); - return testCases.Select(tc => new WcfTestCase(tc, skippedReason)); + return testCases.Select(tc => new WcfTestCase((XunitTestCase)tc, + discoveryOptions.MethodDisplayOrDefault(), + skippedReason, + isTheory, + diagnosticMessageSink)); } } // If we get this far, we have decided to run the test. // Still wrap it in a WcfTestCase with a null skip message // so that other WcfTestCase customizations are used. - return testCases.Select(tc => new WcfTestCase(tc, + return testCases.Select(tc => new WcfTestCase(testCase: (XunitTestCase)tc, + defaultMethodDisplay: discoveryOptions.MethodDisplayOrDefault(), skippedReason: null, isTheory: isTheory, diagnosticMessageSink: diagnosticMessageSink)); diff --git a/src/System.Private.ServiceModel/tests/Common/Scenarios/ThreadHoppingSynchronizationContext.cs b/src/System.Private.ServiceModel/tests/Common/Scenarios/ThreadHoppingSynchronizationContext.cs new file mode 100644 index 00000000000..70b39d3998c --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Common/Scenarios/ThreadHoppingSynchronizationContext.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; + +namespace ScenarioTests.Common +{ + public class ThreadHoppingSynchronizationContext : SynchronizationContext + { + public static ThreadHoppingSynchronizationContext Instance { get; } = new ThreadHoppingSynchronizationContext(); + + public override void Post(SendOrPostCallback d, object state) + { + var t = new Thread(CallbackRunner); + t.Start((d, state)); + } + + public override void Send(SendOrPostCallback d, object state) + { + var t = new Thread(CallbackRunner); + t.Start((d, state)); + } + + public static void CallbackRunner(object param) + { + (SendOrPostCallback d, object state) = ((SendOrPostCallback, object))param; + d(state); + } + } +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/Binding.Http.IntegrationTests.csproj b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/Binding.Http.IntegrationTests.csproj index 130f7473c94..d69ae022b56 100644 --- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/Binding.Http.IntegrationTests.csproj +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/Binding.Http.IntegrationTests.csproj @@ -5,8 +5,8 @@ false true false + 9.0 - diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/ModuleInitializerAttribute.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/ModuleInitializerAttribute.cs new file mode 100644 index 00000000000..e9469418721 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/ModuleInitializerAttribute.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Runtime.CompilerServices +{ +#if !NET5_0_OR_GREATER + public class ModuleInitializerAttribute : Attribute + { + } +#endif +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/OperationContextTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/OperationContextTests.cs new file mode 100644 index 00000000000..6d583998801 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Http/OperationContextTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.ServiceModel; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Common; +using ScenarioTests.Common; +using Xunit; + +public static class OperationContextTests +{ +#pragma warning disable xUnit1013 // Public method should be marked as test + [ModuleInitializer] + public static void InitializeModule() + { + AppContext.SetSwitch("System.ServiceModel.OperationContext.DisableAsyncFlow", true); + } +#pragma warning restore xUnit1013 // Public method should be marked as test + + // This test verifies that using the AppContext switch does revert to the old behavior and has the + // same 2 failure modes (not flowing past an async, and disposing the OperationContextScope throws + // after a thread switch. + // This test lives in the scenario tests as it needs a real channel to test OperationContext + // even though nothing is being sent across the wire. + [WcfFact] + public static async Task OperationContextLegacyBehavior() + { + ChannelFactory factory = null; + IWcfService serviceProxy = null; + var exisitingSyncContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(ThreadHoppingSynchronizationContext.Instance); + bool asyncFlowDisabled = AppContext.TryGetSwitch("System.ServiceModel.OperationContext.DisableAsyncFlow", out bool switchEnabled) && switchEnabled; + Assert.True(asyncFlowDisabled, "Async flow of Operation Context isn't disabled"); + var binding = new BasicHttpBinding(BasicHttpSecurityMode.None); + factory = new ChannelFactory(binding, new EndpointAddress(Endpoints.HttpBaseAddress_Basic)); + serviceProxy = factory.CreateChannel(); + ((IClientChannel)serviceProxy).Open(); + Assert.Null(OperationContext.Current); + var scope = new OperationContextScope((IContextChannel)serviceProxy); + Assert.NotNull(OperationContext.Current); + var currentContext = OperationContext.Current; + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Yield(); + Assert.NotEqual(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.NotEqual(currentContext, OperationContext.Current); + Assert.Throws(() => scope.Dispose()); + ((IClientChannel)serviceProxy).Close(); + factory.Close(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(exisitingSyncContext); + await Task.Yield(); // Hop back to original sync context + ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); + } + } +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.0.0.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.0.0.cs index bdf8a4809c6..6c48c994542 100644 --- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.0.0.cs +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.0.0.cs @@ -40,4 +40,4 @@ public static void SecurityModeNone_Echo_RoundTrips_String() ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); } } -} \ No newline at end of file +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.1.0.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.1.0.cs index dde75102c2b..b306ce7f62b 100644 --- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.1.0.cs +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/NetTcpBindingTests.4.1.0.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using Infrastructure.Common; using System.ServiceModel; using Xunit; @@ -81,4 +80,4 @@ public static void SecurityModeTransport_Echo_RoundTrips_String() ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); } } -} \ No newline at end of file +} diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/OperationContextAsyncFlowTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/OperationContextAsyncFlowTests.cs new file mode 100644 index 00000000000..e2307632773 --- /dev/null +++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/Tcp/OperationContextAsyncFlowTests.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.ExceptionServices; +using System.ServiceModel; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Common; +using ScenarioTests.Common; +using Xunit; + +public class OperationContextAsyncFlowTests +{ + [WcfFact] + public static async Task OperationContextScopeAsyncFlow() + { + // This test lives in the scenario tests as it needs a real channel to test OperationContext + // even though nothing is being sent across the wire. + ChannelFactory factory = null; + IWcfService serviceProxy = null; + var exisitingSyncContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(ThreadHoppingSynchronizationContext.Instance); + NetTcpBinding binding = new NetTcpBinding(); + factory = new ChannelFactory(binding, new EndpointAddress(Endpoints.Tcp_DefaultBinding_Address)); + serviceProxy = factory.CreateChannel(); + Assert.Null(OperationContext.Current); + using (var scope = new OperationContextScope((IContextChannel)serviceProxy)) + { + Assert.NotNull(OperationContext.Current); + var currentContext = OperationContext.Current; + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Yield(); + Assert.NotEqual(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(currentContext, OperationContext.Current); + } + ((IClientChannel)serviceProxy).Close(); + factory.Close(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(exisitingSyncContext); + await Task.Yield(); // Hop back to original sync context + ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); + } + } + + [WcfFact] + public static async Task OperationContextScopeNestedAsyncFlow() + { + // This test lives in the scenario tests as it needs a real channel to test OperationContext + // even though nothing is being sent across the wire. + ChannelFactory factory = null; + IWcfService serviceProxy = null; + var exisitingSyncContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(ThreadHoppingSynchronizationContext.Instance); + NetTcpBinding binding = new NetTcpBinding(); + factory = new ChannelFactory(binding, new EndpointAddress(Endpoints.Tcp_DefaultBinding_Address)); + serviceProxy = factory.CreateChannel(); + Assert.Null(OperationContext.Current); + using (var scope = new OperationContextScope((IContextChannel)serviceProxy)) + { + Assert.NotNull(OperationContext.Current); + var firstContext = OperationContext.Current; + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Yield(); + Assert.NotEqual(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(firstContext, OperationContext.Current); + using (var scope2 = new OperationContextScope((IContextChannel)serviceProxy)) + { + Assert.NotEqual(firstContext, OperationContext.Current); + var secondContext = OperationContext.Current; + currentThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Yield(); + Assert.NotEqual(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(secondContext, OperationContext.Current); + } + Assert.Equal(firstContext, OperationContext.Current); + } + Assert.Null(OperationContext.Current); + ((IClientChannel)serviceProxy).Close(); + factory.Close(); + } + finally + { + // *** ENSURE CLEANUP *** \\ + SynchronizationContext.SetSynchronizationContext(exisitingSyncContext); + await Task.Yield(); // Hop back to original sync context + ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); + } + } + + + [WcfFact] + public static async Task OperationContextCallbackAsyncFlow() + { + ChannelFactory factory = null; + IDuplexChannelService serviceProxy = null; + Guid guid = Guid.NewGuid(); + + try + { + // *** SETUP *** \\ + NetTcpBinding binding = new NetTcpBinding(); + binding.Security.Mode = SecurityMode.None; + DuplexChannelServiceCallback callbackService = new DuplexChannelServiceCallback(); + InstanceContext context = new InstanceContext(callbackService); + EndpointAddress endpointAddress = new EndpointAddress(Endpoints.Tcp_NoSecurity_DuplexCallback_Address); + factory = new DuplexChannelFactory(context, binding, endpointAddress); + serviceProxy = factory.CreateChannel(); + + // *** EXECUTE *** \\ + serviceProxy.Ping(guid); + await callbackService.CallCompletedTask; + + // *** VALIDATE *** \\ + if (callbackService.Exception != null) + { + ExceptionDispatchInfo.Capture(callbackService.Exception).Throw(); + } + + // *** CLEANUP *** \\ + ((ICommunicationObject)serviceProxy).Close(); + factory.Close(); + } + finally + { + // *** ENSURE CLEANUP *** \\ + ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); + } + } + + public class DuplexChannelServiceCallback : IDuplexChannelCallback + { + private TaskCompletionSource _tcs; + + public DuplexChannelServiceCallback() + { + _tcs = new TaskCompletionSource(); + } + + public Exception Exception { get; private set; } + + public Task CallCompletedTask => _tcs.Task; + + public async Task OnPingCallbackAsync(Guid guid) + { + var exisitingSyncContext = SynchronizationContext.Current; + try + { + SynchronizationContext.SetSynchronizationContext(ThreadHoppingSynchronizationContext.Instance); + var asyncFlowDisabled = AppContext.TryGetSwitch("System.ServiceModel.OperationContext.DisableAsyncFlow", out var enabled) && enabled; + Assert.False(asyncFlowDisabled, "DisableAsyncFlow should not be set to true"); + var opContext = OperationContext.Current; + Assert.NotNull(opContext); + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Yield(); + Assert.NotEqual(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(opContext, OperationContext.Current); + _tcs.TrySetResult(null); + } + catch (Exception e) + { + Exception = e; + } + finally + { + // *** ENSURE CLEANUP *** \\ + SynchronizationContext.SetSynchronizationContext(exisitingSyncContext); + await Task.Yield(); // Hop back to original sync context + } + } + } + + [ServiceContract(CallbackContract = typeof(IDuplexChannelCallback))] + public interface IDuplexChannelService + { + [OperationContract(IsOneWay = true)] + void Ping(Guid guid); + } + + public interface IDuplexChannelCallback + { + [OperationContract(IsOneWay = true)] + Task OnPingCallbackAsync(Guid guid); + } +}