1 //------------------------------------------------------------------------------
2 // <copyright file="AspNetSynchronizationContext.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11 using System.Runtime.ExceptionServices;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using System.Web.Util;
16 internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase {
18 // we move all of the state to a separate field since our CreateCopy() method needs shallow copy semantics
19 private readonly State _state;
21 internal AspNetSynchronizationContext(ISyncContext syncContext)
22 : this(new State(new SynchronizationHelper(syncContext))) {
25 private AspNetSynchronizationContext(State state) {
29 internal override bool AllowAsyncDuringSyncStages {
31 return _state.AllowAsyncDuringSyncStages;
34 _state.AllowAsyncDuringSyncStages = value;
38 // We can't ever truly disable the AspNetSynchronizationContext, as the user and runtime can kick off asynchronous
39 // operations whether we wanted them to or not. But this property can be used as a flag by Page and other types
40 // to signal that asynchronous operations are not currently valid, so at least ASP.NET can avoid kicking them
41 // off and can bubble an appropriate exception back to the developer.
42 internal override bool Enabled {
43 get { return _state.Enabled; }
46 internal override ExceptionDispatchInfo ExceptionDispatchInfo {
47 get { return _state.Helper.Error; }
50 internal override int PendingOperationsCount {
51 get { return _state.Helper.PendingCount; }
54 internal override void AllowVoidAsyncOperations() {
55 _state.AllowVoidAsyncOperations = true;
58 [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", Justification = "Used only during debug.")]
59 internal override void AssociateWithCurrentThread() {
60 IDisposable disassociationAction = _state.Helper.EnterSynchronousControl();
63 IDisposable capturedDisassociationAction = disassociationAction;
64 Thread capturedThread = Thread.CurrentThread;
65 disassociationAction = new DisposableAction(() => {
66 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));
67 capturedDisassociationAction.Dispose();
71 // Don't need to synchronize access to SyncControlDisassociationActions since only one thread can call
72 // EnterSynchronousControl() at a time.
73 _state.SyncControlDisassociationActions.Push(disassociationAction);
76 internal override void ClearError() {
77 _state.Helper.Error = null;
80 // Called by the BCL when it needs a SynchronizationContext that is identical to the existing context
81 // but does not have referential equality.
82 public override SynchronizationContext CreateCopy() {
83 return new AspNetSynchronizationContext(_state);
86 internal override void Disable() {
87 _state.Enabled = false;
90 internal override void DisassociateFromCurrentThread() {
91 // Don't need to synchronize access to SyncControlDisassociationActions since we assume that our callers are
92 // well-behaved and won't call DisassociateFromCurrentThread() on a thread other than the one which called
93 // AssociateWithCurrentThread(), which itself serializes access.
94 Debug.Assert(_state.SyncControlDisassociationActions.Count > 0, "DisassociateFromCurrentThread() was called on a thread which hadn't previously called AssociateWithCurrentThread().");
95 IDisposable disassociationAction = _state.SyncControlDisassociationActions.Pop();
96 disassociationAction.Dispose();
99 internal override void Enable() {
100 _state.Enabled = true;
103 public override void OperationCompleted() {
104 Interlocked.Decrement(ref _state.VoidAsyncOutstandingOperationCount); // this line goes first since ChangeOperationCount might invoke a callback which depends on this value
105 _state.Helper.ChangeOperationCount(-1);
108 public override void OperationStarted() {
109 // If the caller tries to kick off an asynchronous operation while we are not
110 // processing an async module, handler, or Page, we should prohibit the operation.
111 if (!AllowAsyncDuringSyncStages && !_state.AllowVoidAsyncOperations) {
112 InvalidOperationException ex = new InvalidOperationException(SR.GetString(SR.Async_operation_cannot_be_started));
116 _state.Helper.ChangeOperationCount(+1);
117 Interlocked.Increment(ref _state.VoidAsyncOutstandingOperationCount);
120 // Dev11 Bug 70908: Race condition involving SynchronizationContext allows ASP.NET requests to be abandoned in the pipeline
122 // When the last completion occurs, the _pendingCount is decremented and then the _lastCompletionCallbackLock is acquired to get
123 // the _lastCompletionCallback. If the _lastCompletionCallback is non-null, then the last completion will invoke the callback;
124 // otherwise, the caller of PendingCompletion will handle the completion.
125 internal override bool PendingCompletion(WaitCallback callback) {
126 return _state.Helper.TrySetCompletionContinuation(() => callback(null));
129 public override void Post(SendOrPostCallback callback, Object state) {
130 _state.Helper.QueueAsynchronous(() => callback(state));
133 // The method is used to post async func.
134 internal void PostAsync(Func<object, Task> callback, Object state) {
135 _state.Helper.QueueAsynchronousAsync(callback, state);
138 internal override void ProhibitVoidAsyncOperations() {
139 _state.AllowVoidAsyncOperations = false;
141 // If the caller tries to prohibit async operations while there are still some
142 // outstanding, we should treat this as an error condition. We can't throw from
143 // this method since (a) the caller generally isn't prepared for it and (b) we
144 // need to wait for the outstanding operations to finish anyway, so we instead
145 // need to mark the helper as faulted.
147 // There is technically a race condition here: the caller isn't guaranteed to
148 // observe the error if the operation counter hits zero at just the right time.
149 // But it's actually not terrible if that happens, since the error is really
150 // just meant to be used for diagnostic purposes.
151 if (!AllowAsyncDuringSyncStages && Volatile.Read(ref _state.VoidAsyncOutstandingOperationCount) > 0) {
152 InvalidOperationException ex = new InvalidOperationException(SR.GetString(SR.Async_operation_cannot_be_pending));
153 _state.Helper.Error = ExceptionDispatchInfo.Capture(ex);
157 internal override void ResetSyncCaller() {
159 // this type doesn't special-case asynchronous work kicked off from a synchronous handler
162 internal override void SetSyncCaller() {
164 // this type doesn't special-case asynchronous work kicked off from a synchronous handler
167 public override void Send(SendOrPostCallback callback, Object state) {
168 _state.Helper.QueueSynchronous(() => callback(state));
171 private sealed class State {
172 internal bool AllowAsyncDuringSyncStages = AppSettings.AllowAsyncDuringSyncStages;
173 internal volatile bool AllowVoidAsyncOperations = false;
174 internal bool Enabled = true;
175 internal readonly SynchronizationHelper Helper; // handles scheduling of the asynchronous tasks
176 internal Stack<IDisposable> SyncControlDisassociationActions = new Stack<IDisposable>(capacity: 1);
177 internal int VoidAsyncOutstandingOperationCount = 0;
179 internal State(SynchronizationHelper helper) {