// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. using System.Reactive.Disposables; using System.Threading; using System.Runtime.CompilerServices; using System.Reactive.Concurrency; namespace System.Reactive.Subjects { /// /// Represents the result of an asynchronous operation. /// The last value before the OnCompleted notification, or the error received through OnError, is sent to all subscribed observers. /// /// The type of the elements processed by the subject. public sealed class AsyncSubject : ISubject, IDisposable #if HAS_AWAIT , INotifyCompletion #endif { private readonly object _gate = new object(); private ImmutableList> _observers; private bool _isDisposed; private bool _isStopped; private T _value; private bool _hasValue; private Exception _exception; /// /// Creates a subject that can only receive one value and that value is cached for all future observations. /// public AsyncSubject() { _observers = new ImmutableList>(); } /// /// Indicates whether the subject has observers subscribed to it. /// public bool HasObservers { get { var observers = _observers; return observers != null && observers.Data.Length > 0; } } /// /// Notifies all subscribed observers about the end of the sequence, also causing the last received value to be sent out (if any). /// public void OnCompleted() { var os = default(IObserver[]); var v = default(T); var hv = false; lock (_gate) { CheckDisposed(); if (!_isStopped) { os = _observers.Data; _observers = new ImmutableList>(); _isStopped = true; v = _value; hv = _hasValue; } } if (os != null) { if (hv) { foreach (var o in os) { o.OnNext(v); o.OnCompleted(); } } else foreach (var o in os) o.OnCompleted(); } } /// /// Notifies all subscribed observers about the exception. /// /// The exception to send to all observers. /// is null. public void OnError(Exception error) { if (error == null) throw new ArgumentNullException("error"); var os = default(IObserver[]); lock (_gate) { CheckDisposed(); if (!_isStopped) { os = _observers.Data; _observers = new ImmutableList>(); _isStopped = true; _exception = error; } } if (os != null) foreach (var o in os) o.OnError(error); } /// /// Sends a value to the subject. The last value received before successful termination will be sent to all subscribed and future observers. /// /// The value to store in the subject. public void OnNext(T value) { lock (_gate) { CheckDisposed(); if (!_isStopped) { _value = value; _hasValue = true; } } } /// /// Subscribes an observer to the subject. /// /// Observer to subscribe to the subject. /// Disposable object that can be used to unsubscribe the observer from the subject. /// is null. public IDisposable Subscribe(IObserver observer) { if (observer == null) throw new ArgumentNullException("observer"); var ex = default(Exception); var v = default(T); var hv = false; lock (_gate) { CheckDisposed(); if (!_isStopped) { _observers = _observers.Add(observer); return new Subscription(this, observer); } ex = _exception; hv = _hasValue; v = _value; } if (ex != null) observer.OnError(ex); else if (hv) { observer.OnNext(v); observer.OnCompleted(); } else observer.OnCompleted(); return Disposable.Empty; } class Subscription : IDisposable { private readonly AsyncSubject _subject; private IObserver _observer; public Subscription(AsyncSubject subject, IObserver observer) { _subject = subject; _observer = observer; } public void Dispose() { if (_observer != null) { lock (_subject._gate) { if (!_subject._isDisposed && _observer != null) { _subject._observers = _subject._observers.Remove(_observer); _observer = null; } } } } } void CheckDisposed() { if (_isDisposed) throw new ObjectDisposedException(string.Empty); } /// /// Unsubscribe all observers and release resources. /// public void Dispose() { lock (_gate) { _isDisposed = true; _observers = null; _exception = null; _value = default(T); } } #if HAS_AWAIT /// /// Gets an awaitable object for the current AsyncSubject. /// /// Object that can be awaited. public AsyncSubject GetAwaiter() { return this; } /// /// Specifies a callback action that will be invoked when the subject completes. /// /// Callback action that will be invoked when the subject completes. /// is null. public void OnCompleted(Action continuation) { if (continuation == null) throw new ArgumentNullException("continuation"); OnCompleted(continuation, true); } #endif private void OnCompleted(Action continuation, bool originalContext) { // // [OK] Use of unsafe Subscribe: this type's Subscribe implementation is safe. // this.Subscribe/*Unsafe*/(new AwaitObserver(continuation, originalContext)); } class AwaitObserver : IObserver { #if HAS_AWAIT private readonly SynchronizationContext _context; #endif private readonly Action _callback; public AwaitObserver(Action callback, bool originalContext) { #if HAS_AWAIT if (originalContext) _context = SynchronizationContext.Current; #else System.Diagnostics.Debug.Assert(!originalContext); #endif _callback = callback; } public void OnCompleted() { InvokeOnOriginalContext(); } public void OnError(Exception error) { InvokeOnOriginalContext(); } public void OnNext(T value) { } private void InvokeOnOriginalContext() { #if HAS_AWAIT if (_context != null) { // // No need for OperationStarted and OperationCompleted calls here; // this code is invoked through await support and will have a way // to observe its start/complete behavior, either through returned // Task objects or the async method builder's interaction with the // SynchronizationContext object. // _context.Post(c => ((Action)c)(), _callback); } else #endif { _callback(); } } } /// /// Gets whether the AsyncSubject has completed. /// public bool IsCompleted { get { return _isStopped; } } /// /// Gets the last element of the subject, potentially blocking until the subject completes successfully or exceptionally. /// /// The last element of the subject. Throws an InvalidOperationException if no element was received. /// The source sequence is empty. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Await pattern for C# and VB compilers.")] public T GetResult() { if (!_isStopped) { var e = new ManualResetEvent(false); OnCompleted(() => e.Set(), false); e.WaitOne(); } _exception.ThrowIfNotNull(); if (!_hasValue) throw new InvalidOperationException(Strings_Linq.NO_ELEMENTS); return _value; } } }