//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using System.Web.Util; internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase { // we move all of the state to a separate field since our CreateCopy() method needs shallow copy semantics private readonly State _state; internal AspNetSynchronizationContext(ISyncContext syncContext) : this(new State(new SynchronizationHelper(syncContext))) { } private AspNetSynchronizationContext(State state) { _state = state; } internal override bool AllowAsyncDuringSyncStages { get { return _state.AllowAsyncDuringSyncStages; } set { _state.AllowAsyncDuringSyncStages = value; } } // We can't ever truly disable the AspNetSynchronizationContext, as the user and runtime can kick off asynchronous // operations whether we wanted them to or not. But this property can be used as a flag by Page and other types // to signal that asynchronous operations are not currently valid, so at least ASP.NET can avoid kicking them // off and can bubble an appropriate exception back to the developer. internal override bool Enabled { get { return _state.Enabled; } } internal override ExceptionDispatchInfo ExceptionDispatchInfo { get { return _state.Helper.Error; } } internal override int PendingOperationsCount { get { return _state.Helper.PendingCount; } } internal override void AllowVoidAsyncOperations() { _state.AllowVoidAsyncOperations = true; } [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", Justification = "Used only during debug.")] internal override void AssociateWithCurrentThread() { IDisposable disassociationAction = _state.Helper.EnterSynchronousControl(); #if DBG IDisposable capturedDisassociationAction = disassociationAction; Thread capturedThread = Thread.CurrentThread; disassociationAction = new DisposableAction(() => { Debug.Assert(capturedThread == Thread.CurrentThread, String.Format("AssociateWithCurrentThread was called on thread ID '{0}', but DisassociateFromCurrentThread was called on thread ID '{1}'.", capturedThread.ManagedThreadId, Thread.CurrentThread.ManagedThreadId)); capturedDisassociationAction.Dispose(); }); #endif // Don't need to synchronize access to SyncControlDisassociationActions since only one thread can call // EnterSynchronousControl() at a time. _state.SyncControlDisassociationActions.Push(disassociationAction); } internal override void ClearError() { _state.Helper.Error = null; } // Called by the BCL when it needs a SynchronizationContext that is identical to the existing context // but does not have referential equality. public override SynchronizationContext CreateCopy() { return new AspNetSynchronizationContext(_state); } internal override void Disable() { _state.Enabled = false; } internal override void DisassociateFromCurrentThread() { // Don't need to synchronize access to SyncControlDisassociationActions since we assume that our callers are // well-behaved and won't call DisassociateFromCurrentThread() on a thread other than the one which called // AssociateWithCurrentThread(), which itself serializes access. Debug.Assert(_state.SyncControlDisassociationActions.Count > 0, "DisassociateFromCurrentThread() was called on a thread which hadn't previously called AssociateWithCurrentThread()."); IDisposable disassociationAction = _state.SyncControlDisassociationActions.Pop(); disassociationAction.Dispose(); } internal override void Enable() { _state.Enabled = true; } public override void OperationCompleted() { Interlocked.Decrement(ref _state.VoidAsyncOutstandingOperationCount); // this line goes first since ChangeOperationCount might invoke a callback which depends on this value _state.Helper.ChangeOperationCount(-1); } public override void OperationStarted() { // If the caller tries to kick off an asynchronous operation while we are not // processing an async module, handler, or Page, we should prohibit the operation. if (!AllowAsyncDuringSyncStages && !_state.AllowVoidAsyncOperations) { InvalidOperationException ex = new InvalidOperationException(SR.GetString(SR.Async_operation_cannot_be_started)); throw ex; } _state.Helper.ChangeOperationCount(+1); Interlocked.Increment(ref _state.VoidAsyncOutstandingOperationCount); } // Dev11 Bug 70908: Race condition involving SynchronizationContext allows ASP.NET requests to be abandoned in the pipeline // // When the last completion occurs, the _pendingCount is decremented and then the _lastCompletionCallbackLock is acquired to get // the _lastCompletionCallback. If the _lastCompletionCallback is non-null, then the last completion will invoke the callback; // otherwise, the caller of PendingCompletion will handle the completion. internal override bool PendingCompletion(WaitCallback callback) { return _state.Helper.TrySetCompletionContinuation(() => callback(null)); } public override void Post(SendOrPostCallback callback, Object state) { _state.Helper.QueueAsynchronous(() => callback(state)); } // The method is used to post async func. internal void PostAsync(Func callback, Object state) { _state.Helper.QueueAsynchronousAsync(callback, state); } internal override void ProhibitVoidAsyncOperations() { _state.AllowVoidAsyncOperations = false; // If the caller tries to prohibit async operations while there are still some // outstanding, we should treat this as an error condition. We can't throw from // this method since (a) the caller generally isn't prepared for it and (b) we // need to wait for the outstanding operations to finish anyway, so we instead // need to mark the helper as faulted. // // There is technically a race condition here: the caller isn't guaranteed to // observe the error if the operation counter hits zero at just the right time. // But it's actually not terrible if that happens, since the error is really // just meant to be used for diagnostic purposes. if (!AllowAsyncDuringSyncStages && Volatile.Read(ref _state.VoidAsyncOutstandingOperationCount) > 0) { InvalidOperationException ex = new InvalidOperationException(SR.GetString(SR.Async_operation_cannot_be_pending)); _state.Helper.Error = ExceptionDispatchInfo.Capture(ex); } } internal override void ResetSyncCaller() { // no-op // this type doesn't special-case asynchronous work kicked off from a synchronous handler } internal override void SetSyncCaller() { // no-op // this type doesn't special-case asynchronous work kicked off from a synchronous handler } public override void Send(SendOrPostCallback callback, Object state) { _state.Helper.QueueSynchronous(() => callback(state)); } private sealed class State { internal bool AllowAsyncDuringSyncStages = AppSettings.AllowAsyncDuringSyncStages; internal volatile bool AllowVoidAsyncOperations = false; internal bool Enabled = true; internal readonly SynchronizationHelper Helper; // handles scheduling of the asynchronous tasks internal Stack SyncControlDisassociationActions = new Stack(capacity: 1); internal int VoidAsyncOutstandingOperationCount = 0; internal State(SynchronizationHelper helper) { Helper = helper; } } } }