@@ -67,15 +67,16 @@ private HubConnection CreateHubConnection(
6767 return hubConnectionBuilder . Build ( ) ;
6868 }
6969
70- private Func < EndPoint , ValueTask < ConnectionContext > > GetHttpConnectionFactory ( string url , ILoggerFactory loggerFactory , string path , HttpTransportType transportType , TransferFormat transferFormat )
70+ private static Func < EndPoint , ValueTask < ConnectionContext > > GetHttpConnectionFactory ( string url , ILoggerFactory loggerFactory , string path , HttpTransportType transportType , TransferFormat transferFormat )
7171 {
7272 return async endPoint =>
7373 {
7474 var httpEndpoint = ( UriEndPoint ) endPoint ;
7575 var options = new HttpConnectionOptions { Url = httpEndpoint . Uri , Transports = transportType , DefaultTransferFormat = transferFormat } ;
7676 var connection = new HttpConnection ( options , loggerFactory ) ;
7777
78- await connection . StartAsync ( ) ;
78+ // This is used by CanBlockOnAsyncOperationsWithOneAtATimeSynchronizationContext, so the ConfigureAwait(false) is important.
79+ await connection . StartAsync ( ) . ConfigureAwait ( false ) ;
7980
8081 return connection ;
8182 } ;
@@ -2090,6 +2091,90 @@ bool ExpectedErrors(WriteContext writeContext)
20902091 }
20912092 }
20922093
2094+ [ Theory ]
2095+ [ MemberData ( nameof ( TransportTypes ) ) ]
2096+ public async Task CanBlockOnAsyncOperationsWithOneAtATimeSynchronizationContext ( HttpTransportType transportType )
2097+ {
2098+ const int DefaultTimeout = Testing . TaskExtensions . DefaultTimeoutDuration ;
2099+
2100+ await using var server = await StartServer < Startup > ( ) ;
2101+ await using var connection = CreateHubConnection ( server . Url , "/default" , transportType , HubProtocols [ "json" ] , LoggerFactory ) ;
2102+ await using var oneAtATimeSynchronizationContext = new OneAtATimeSynchronizationContext ( ) ;
2103+
2104+ var originalSynchronizationContext = SynchronizationContext . Current ;
2105+ SynchronizationContext . SetSynchronizationContext ( oneAtATimeSynchronizationContext ) ;
2106+
2107+ try
2108+ {
2109+ // Yield first so the rest of the test runs in the OneAtATimeSynchronizationContext.Run loop
2110+ await Task . Yield ( ) ;
2111+
2112+ Assert . True ( connection . StartAsync ( ) . Wait ( DefaultTimeout ) ) ;
2113+
2114+ var invokeTask = connection . InvokeAsync < string > ( nameof ( TestHub . HelloWorld ) ) ;
2115+ Assert . True ( invokeTask . Wait ( DefaultTimeout ) ) ;
2116+ Assert . Equal ( "Hello World!" , invokeTask . Result ) ;
2117+
2118+ Assert . True ( connection . DisposeAsync ( ) . AsTask ( ) . Wait ( DefaultTimeout ) ) ;
2119+ }
2120+ catch ( Exception ex )
2121+ {
2122+ LoggerFactory . CreateLogger < HubConnectionTests > ( ) . LogError ( ex , "{ExceptionType} from test" , ex . GetType ( ) . FullName ) ;
2123+ throw ;
2124+ }
2125+ finally
2126+ {
2127+ SynchronizationContext . SetSynchronizationContext ( originalSynchronizationContext ) ;
2128+ }
2129+ }
2130+
2131+ private class OneAtATimeSynchronizationContext : SynchronizationContext , IAsyncDisposable
2132+ {
2133+ private readonly Channel < ( SendOrPostCallback , object ) > _taskQueue = Channel . CreateUnbounded < ( SendOrPostCallback , object ) > ( ) ;
2134+ private readonly Task _runTask ;
2135+ private bool _disposed ;
2136+
2137+ public OneAtATimeSynchronizationContext ( )
2138+ {
2139+ // Task.Run to avoid running with xUnit's AsyncTestSyncContext as well.
2140+ _runTask = Task . Run ( Run ) ;
2141+ }
2142+
2143+ public override void Post ( SendOrPostCallback d , object state )
2144+ {
2145+ if ( _disposed )
2146+ {
2147+ // There should be no other calls to Post() after dispose. If there are calls,
2148+ // the test has most likely failed with a timeout. Let the callbacks run so the
2149+ // timeout exception gets reported accurately instead of as a long-running test.
2150+ d ( state ) ;
2151+ }
2152+
2153+ _taskQueue . Writer . TryWrite ( ( d , state ) ) ;
2154+ }
2155+
2156+ public ValueTask DisposeAsync ( )
2157+ {
2158+ _disposed = true ;
2159+ _taskQueue . Writer . Complete ( ) ;
2160+ return new ValueTask ( _runTask ) ;
2161+ }
2162+
2163+ private async Task Run ( )
2164+ {
2165+ while ( await _taskQueue . Reader . WaitToReadAsync ( ) )
2166+ {
2167+ SetSynchronizationContext ( this ) ;
2168+ while ( _taskQueue . Reader . TryRead ( out var tuple ) )
2169+ {
2170+ var ( callback , state ) = tuple ;
2171+ callback ( state ) ;
2172+ }
2173+ SetSynchronizationContext ( null ) ;
2174+ }
2175+ }
2176+ }
2177+
20932178 private class PollTrackingMessageHandler : DelegatingHandler
20942179 {
20952180 public Task < HttpResponseMessage > ActivePoll { get ; private set ; }
0 commit comments