1 //------------------------------------------------------------------------------
2 // <copyright file="_ContextAwareResult.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
8 using System.Threading;
10 using System.Security.Principal;
11 using System.Security.Permissions;
14 // This is used by ContextAwareResult to cache callback closures between similar calls. Create one of these and
15 // pass it in to FinishPostingAsyncOp() to prevent the context from being captured in every iteration of a looped async call.
17 // I thought about making the delegate and state into weak references, but decided against it because:
18 // - The delegate is very likely to be abandoned by the user right after calling BeginXxx, making caching it useless. There's
19 // no easy way to weakly reference just the target.
20 // - We want to support identifying state via object.Equals() (especially value types), which means we need to keep a
21 // reference to the original. Plus, if we're holding the target, might as well hold the state too.
22 // The user will need to disable caching if they want their target/state to be instantly collected.
24 // For now the state is not included as part of the closure. It is too common a pattern (for example with socket receive)
25 // to have several pending IOs differentiated by their state object. We don't want that pattern to break the cache.
27 internal class CallbackClosure
29 private AsyncCallback savedCallback;
30 private ExecutionContext savedContext;
32 internal CallbackClosure(ExecutionContext context, AsyncCallback callback)
36 savedCallback = callback;
37 savedContext = context;
41 internal bool IsCompatible(AsyncCallback callback)
43 if (callback == null || savedCallback == null)
46 // Delegates handle this ok. AsyncCallback is sealed and immutable, so if this succeeds, we are safe to use
47 // the passed-in instance.
48 if (!object.Equals(savedCallback, callback))
54 internal AsyncCallback AsyncCallback
62 internal ExecutionContext Context
72 // This class will ensure that the correct context is restored on the thread before invoking
75 internal class ContextAwareResult : LazyAsyncResult
78 private enum StateFlags
81 CaptureIdentity = 0x01,
82 CaptureContext = 0x02,
83 ThreadSafeContextCopy = 0x04,
84 PostBlockStarted = 0x08,
85 PostBlockFinished = 0x10,
88 // This needs to be volatile so it's sure to make it over to the completion thread in time.
89 private volatile ExecutionContext _Context;
91 private StateFlags _Flags;
94 private WindowsIdentity _Wi;
97 internal ContextAwareResult(object myObject, object myState, AsyncCallback myCallBack) :
98 this(false, false, myObject, myState, myCallBack)
101 // Setting captureIdentity enables the Identity property. This will be available even if ContextCopy isn't, either because
102 // flow is suppressed or it wasn't needed. (If ContextCopy isn't available, Identity may or may not be. But if it is, it
103 // should be used instead of ContextCopy for impersonation - ContextCopy might not include the identity.)
105 // Setting forceCaptureContext enables the ContextCopy property even when a null callback is specified. (The context is
106 // always captured if a callback is given.)
107 internal ContextAwareResult(bool captureIdentity, bool forceCaptureContext, object myObject, object myState, AsyncCallback myCallBack) :
108 this(captureIdentity, forceCaptureContext, false, myObject, myState, myCallBack)
111 internal ContextAwareResult(bool captureIdentity, bool forceCaptureContext, bool threadSafeContextCopy, object myObject, object myState, AsyncCallback myCallBack) :
112 base(myObject, myState, myCallBack)
114 if (forceCaptureContext)
116 _Flags = StateFlags.CaptureContext;
121 _Flags |= StateFlags.CaptureIdentity;
124 if (threadSafeContextCopy)
126 _Flags |= StateFlags.ThreadSafeContextCopy;
131 // Security: We need an assert for a call into WindowsIdentity.GetCurrent
132 [SecurityPermissionAttribute(SecurityAction.Assert, Flags=SecurityPermissionFlag.ControlPrincipal)]
133 private void SafeCaptureIdenity()
135 _Wi = WindowsIdentity.GetCurrent();
140 // This can be used to establish a context during an async op for something like calling a delegate or demanding a permission.
141 // May block briefly if the context is still being produced.
143 // Returns null if called from the posting thread.
145 internal ExecutionContext ContextCopy
149 GlobalLog.Assert(!InternalPeekCompleted || (_Flags & StateFlags.ThreadSafeContextCopy) != 0, "ContextAwareResult#{0}::ContextCopy|Called on completed result.", ValidationHelper.HashString(this));
150 if (InternalPeekCompleted)
152 throw new InvalidOperationException(SR.GetString(SR.net_completed_result));
155 ExecutionContext context = _Context;
158 return context.CreateCopy();
161 // Make sure the context was requested.
162 GlobalLog.Assert(AsyncCallback != null || (_Flags & StateFlags.CaptureContext) != 0, "ContextAwareResult#{0}::ContextCopy|No context captured - specify a callback or forceCaptureContext.", ValidationHelper.HashString(this));
164 // Just use the lock to block. We might be on the thread that owns the lock which is great, it means we
165 // don't need a context anyway.
166 if ((_Flags & StateFlags.PostBlockFinished) == 0)
168 GlobalLog.Assert(_Lock != null, "ContextAwareResult#{0}::ContextCopy|Must lock (StartPostingAsyncOp()) { ... FinishPostingAsyncOp(); } when calling ContextCopy (unless it's only called after FinishPostingAsyncOp).", ValidationHelper.HashString(this));
172 GlobalLog.Assert(!InternalPeekCompleted || (_Flags & StateFlags.ThreadSafeContextCopy) != 0, "ContextAwareResult#{0}::ContextCopy|Result became completed during call.", ValidationHelper.HashString(this));
173 if (InternalPeekCompleted)
175 throw new InvalidOperationException(SR.GetString(SR.net_completed_result));
179 return context == null ? null : context.CreateCopy();
185 // Just like ContextCopy.
187 internal WindowsIdentity Identity
191 GlobalLog.Assert(!InternalPeekCompleted || (_Flags & StateFlags.ThreadSafeContextCopy) != 0, "ContextAwareResult#{0}::Identity|Called on completed result.", ValidationHelper.HashString(this));
192 if (InternalPeekCompleted)
194 throw new InvalidOperationException(SR.GetString(SR.net_completed_result));
202 // Make sure the identity was requested.
203 GlobalLog.Assert((_Flags & StateFlags.CaptureIdentity) != 0, "ContextAwareResult#{0}::Identity|No identity captured - specify captureIdentity.", ValidationHelper.HashString(this));
205 // Just use the lock to block. We might be on the thread that owns the lock which is great, it means we
206 // don't need an identity anyway.
207 if ((_Flags & StateFlags.PostBlockFinished) == 0)
209 GlobalLog.Assert(_Lock != null, "ContextAwareResult#{0}::Identity|Must lock (StartPostingAsyncOp()) { ... FinishPostingAsyncOp(); } when calling Identity (unless it's only called after FinishPostingAsyncOp).", ValidationHelper.HashString(this));
213 GlobalLog.Assert(!InternalPeekCompleted || (_Flags & StateFlags.ThreadSafeContextCopy) != 0, "ContextAwareResult#{0}::Identity|Result became completed during call.", ValidationHelper.HashString(this));
214 if (InternalPeekCompleted)
216 throw new InvalidOperationException(SR.GetString(SR.net_completed_result));
225 // Want to be able to verify that the Identity was requested. If it was requested but isn't available
226 // on the Identity property, it's either available via ContextCopy or wasn't needed (synchronous).
227 internal bool IdentityRequested
231 return (_Flags & StateFlags.CaptureIdentity) != 0;
236 internal object StartPostingAsyncOp()
238 return StartPostingAsyncOp(true);
242 // If ContextCopy or Identity will be used, the return value should be locked until FinishPostingAsyncOp() is called
243 // or the operation has been aborted (e.g. by BeginXxx throwing). Otherwise, this can be called with false to prevent the lock
244 // object from being created.
246 internal object StartPostingAsyncOp(bool lockCapture)
248 GlobalLog.Assert(!InternalPeekCompleted, "ContextAwareResult#{0}::StartPostingAsyncOp|Called on completed result.", ValidationHelper.HashString(this));
250 DebugProtectState(true);
252 _Lock = lockCapture ? new object() : null;
253 _Flags |= StateFlags.PostBlockStarted;
258 // Call this when returning control to the user.
260 internal bool FinishPostingAsyncOp()
262 // Ignore this call if StartPostingAsyncOp() failed or wasn't called, or this has already been called.
263 if ((_Flags & (StateFlags.PostBlockStarted | StateFlags.PostBlockFinished)) != StateFlags.PostBlockStarted)
267 _Flags |= StateFlags.PostBlockFinished;
269 ExecutionContext cachedContext = null;
270 return CaptureOrComplete(ref cachedContext, false);
274 // Call this when returning control to the user. Allows a cached Callback Closure to be supplied and used
275 // as appropriate, and replaced with a new one.
277 internal bool FinishPostingAsyncOp(ref CallbackClosure closure)
279 // Ignore this call if StartPostingAsyncOp() failed or wasn't called, or this has already been called.
280 if ((_Flags & (StateFlags.PostBlockStarted | StateFlags.PostBlockFinished)) != StateFlags.PostBlockStarted)
284 _Flags |= StateFlags.PostBlockFinished;
286 // Need a copy of this ref argument since it can be used in many of these calls simultaneously.
287 CallbackClosure closureCopy = closure;
288 ExecutionContext cachedContext;
289 if (closureCopy == null)
291 cachedContext = null;
295 if (!closureCopy.IsCompatible(AsyncCallback))
297 // Clear the cache as soon as a method is called with incompatible parameters.
299 cachedContext = null;
303 // If it succeeded, we want to replace our context/callback with the one from the closure.
304 // Using the closure's instance of the callback is probably overkill, but safer.
305 AsyncCallback = closureCopy.AsyncCallback;
306 cachedContext = closureCopy.Context;
310 bool calledCallback = CaptureOrComplete(ref cachedContext, true);
312 // Set up new cached context if we didn't use the previous one.
313 if (closure == null && AsyncCallback != null && cachedContext != null)
315 closure = new CallbackClosure(cachedContext, AsyncCallback);
318 return calledCallback;
321 /* enable when needed
323 // Use this to block until FinishPostingAsyncOp() completes. Must check for null.
325 internal object PostingLock
334 // Call this if you want to cancel any flowing.
336 internal void InvokeCallbackWithoutContext(object result)
338 ProtectedInvokeCallback(result, (IntPtr) 1);
343 protected override void Cleanup()
347 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::Cleanup()");
358 // This must be called right before returning the result to the user. It might call the callback itself,
359 // to avoid flowing context. Even if the operation completes before this call, the callback won't have been
362 // Returns whether the operation completed sync or not.
364 private bool CaptureOrComplete(ref ExecutionContext cachedContext, bool returnContext)
366 GlobalLog.Assert((_Flags & StateFlags.PostBlockStarted) != 0, "ContextAwareResult#{0}::CaptureOrComplete|Called without calling StartPostingAsyncOp.", ValidationHelper.HashString(this));
368 // See if we're going to need to capture the context.
369 bool capturingContext = AsyncCallback != null || (_Flags & StateFlags.CaptureContext) != 0;
372 // Peek if we've already completed, but don't fix CompletedSynchronously yet
373 // Capture the identity if requested, unless we're going to capture the context anyway, unless
374 // capturing the context won't be sufficient.
375 if ((_Flags & StateFlags.CaptureIdentity) != 0 && !InternalPeekCompleted &&
376 (!capturingContext || SecurityContext.IsWindowsIdentityFlowSuppressed()))
378 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CaptureOrComplete() starting identity capture");
379 SafeCaptureIdenity();
383 // No need to flow if there's no callback, unless it's been specifically requested.
384 // Note that Capture() can return null, for example if SuppressFlow() is in effect.
385 if (capturingContext && !InternalPeekCompleted)
387 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CaptureOrComplete() starting capture");
388 if (cachedContext == null)
390 cachedContext = ExecutionContext.Capture();
392 if (cachedContext != null)
396 _Context = cachedContext;
397 cachedContext = null;
401 _Context = cachedContext.CreateCopy();
404 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CaptureOrComplete() _Context:" + ValidationHelper.HashString(_Context));
408 // Otherwise we have to have completed synchronously, or not needed the context.
409 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CaptureOrComplete() skipping capture");
410 cachedContext = null;
411 GlobalLog.Assert(AsyncCallback == null || CompletedSynchronously, "ContextAwareResult#{0}::CaptureOrComplete|Didn't capture context, but didn't complete synchronously!", ValidationHelper.HashString(this));
414 // Now we want to see for sure what to do. We might have just captured the context for no reason.
415 // This has to be the first time the state has been queried "for real" (apart from InvokeCallback)
416 // to guarantee synchronization with Complete() (otherwise, Complete() could try to call the
417 // callback without the context having been gotten).
418 DebugProtectState(false);
419 if (CompletedSynchronously)
421 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CaptureOrComplete() completing synchronously");
422 base.Complete(IntPtr.Zero);
430 // Is guaranteed to be called only once. If called with a non-zero userToken, the context is not flowed.
432 protected override void Complete(IntPtr userToken)
434 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::Complete() _Context(set):" + (_Context != null).ToString() + " userToken:" + userToken.ToString());
436 // If no flowing, just complete regularly.
437 if ((_Flags & StateFlags.PostBlockStarted) == 0)
439 base.Complete(userToken);
443 // At this point, IsCompleted is set and CompletedSynchronously is fixed. If it's synchronous, then we want to hold
444 // the completion for the CaptureOrComplete() call to avoid the context flow. If not, we know CaptureOrComplete() has completed.
445 if (CompletedSynchronously)
450 ExecutionContext context = _Context;
452 // If the context is being abandoned or wasn't captured (SuppressFlow, null AsyncCallback), just
453 // complete regularly, as long as CaptureOrComplete() has finished.
457 if (userToken != IntPtr.Zero || context == null)
459 base.Complete(userToken);
463 ExecutionContext.Run((_Flags & StateFlags.ThreadSafeContextCopy) != 0 ? context.CreateCopy() : context,
464 new ContextCallback(CompleteCallback), null);
467 private void CompleteCallback(object state)
469 GlobalLog.Print("ContextAwareResult#" + ValidationHelper.HashString(this) + "::CompleteCallback() Context set, calling callback.");
470 base.Complete(IntPtr.Zero);