Create Public Grafana Dashboards without login

Hi Guys,

There are a bunch of threads out there asking how to make a dashboard public, with the same responses rolled out that it exposes your DB, but then there are also a bunch of examples provided by Grafana on some great public dashboards from the likes of GitLab, Wikimedia, and CERN… link here.

I have tried turning off auth and using anonomyous users, but it doesnt seem to work the same as those linked. Is there a simple tutorial I haven’t found yet for making it both simple for me to link to a live dashboard (not a snapshot, and not a finite time series)? I don’t have sensitive data on my database, and it sits on the cloud, so I’m not exposing my other networked devices. I just want the ability for people with the link to be able to see the dashboard without having to register, but then the ability to log in (if I so choose) and create more dashboards for private and public use (i.e. set some to public, some to private).

Thanks,

2 Likes

I would also like to know this. Specifically we’d like to set up a “playground” to showcase a specific dashboard and allow people to see the queries and potentially modify these (without affecting the “original” dashboard).

Specifically we’re looking to create our own version of the Grafana playground examples - But we’ve found no guide on how to set this up?

1 Like

Anyone? Surely we cant be the only ones that need this? It’s out there, there are publicly accessible dashboards out there…

I asked the support team on this. It seems like it’s possible in the self-hosted version, but not the free cloud version. They wrote the below:

Grafana has an anonymous authentication option, and there is also a “viewer can edit” option to allow viewers to modify panels, but not to save the modifications:


I think it would be beneficial to have the opportunity to change this somehow in the free cloud starter version, though.

1 Like

Absolutely! The private hosted version is to my eyes more or less identical, so it shouldnt be a stretch to add to the cloud version an option to publish some dashboards as public…

It’s almost as if they dont want us to upgrade the cloud model and force us to self host.

I’m prepared to pay for the grafana cloud if i can get this working reliably. Can a dev please help me with this?

This is exactly what I am trying to do. I have no idea what the traffic would look like. Anyone know if paid Grafana cloud will support this?

Hi folks! I can confirm that neither the Grafana Cloud Free plan nor the paid Grafana Cloud Pro subscription offers anonymous viewer access (public dashboards). This is not currently supported since Grafana Cloud is a hosted environment and there are security concerns with allowing unauthenticated access to dashboards.

As the Support team mentioned, self-hosting Grafana OSS gives you full control of these settings so you can choose to allow unauthenticated access if it is absolutely needed, just be cautious with the considerations outlined in the security doc linked above.

1 Like
using System;

namespace PropertyFacadeExample.ViewModel.Features
{
    public sealed class EditWorkingAttendanceViewModel : UXViewModel, ITracksChanges
    {
        public ChangeTracker ChangeTracker { get; } = new ChangeTracker();

        public EditWorkingAttendanceViewModel()
        {
            // Also includes change tracking support, which is optional.

            // DisposeWith registers the subscription with DisposableTracker.
            // This is necessary if you expect the domain model to live longer than the viewmodel.
            // When you close the viewmodel, call DisposableTracker.Dispose().
            //
            // You can dispose a PropertyFacade as many times as you need; this only disposes the underlying
            // subscription if one is present. Likewise you can re-use and re-dispose the tracker.
            //
            // Take care when creating and registering regular Rx subscriptions here in the constructor if you
            // intend to reuse the viewmodel after Dispose. If you dispose those subscriptions, they're gone.

            _adminA = this.PropFacade(vm => vm.AdminA).TrackChanges(this).DisposeWith(this);
            _adminB = this.PropFacade(vm => vm.AdminB).TrackChanges(this).DisposeWith(this);
            _nonSalary = this.PropFacade(vm => vm.NonSalary).TrackChanges(this).DisposeWith(this);
            _salary = this.PropFacade(vm => vm.Salary).TrackChanges(this).DisposeWith(this);
            _travel = this.PropFacade(vm => vm.Travel).TrackChanges(this).DisposeWith(this);
            _leave = this.PropFacade(vm => vm.Leave).TrackChanges(this).DisposeWith(this);
            _total = this.ReadOnlyPropFacade(vm => vm.Total).DisposeWith(this);
        }

        public decimal AdminA { get => _adminA.Value; set => _adminA.Value = value; }
        private readonly PropertyFacade<decimal> _adminA;

        public decimal AdminB { get => _adminB.Value; set => _adminB.Value = value; }
        private readonly PropertyFacade<decimal> _adminB;

        public decimal NonSalary { get => _nonSalary.Value; set => _nonSalary.Value = value;  }
        private readonly PropertyFacade<decimal> _nonSalary;

        public decimal Salary { get => _salary.Value; set => _salary.Value = value; }
        private readonly PropertyFacade<decimal> _salary;

        public decimal Travel { get => _travel.Value; set => _travel.Value = value; }
        private readonly PropertyFacade<decimal> _travel;

        public decimal Leave { get => _leave.Value; set => _leave.Value = value; }
        private readonly PropertyFacade<decimal> _leave;

        public decimal Total => _total.Value;
        private readonly ReadOnlyPropertyFacade<decimal> _total;

        public void Load(Domain.Models.WorkingAttendance model)
        {
            if (model is null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            // PropertyFacade supports hot-swapping subscriptions.
            // If we want to treat the domain model as more-or-less discardable while keeping the viewmodel alive,
            // we can replace an older model with a new one.

            // We can also use ConvertOneWay or ConvertTwoWay extension methods from FacadeConverters.cs to convert value types
            // if the PropertyFacade type and the domain property type do not match. We may want to do this if we
            // need to "play nice" with a UI control that doesn't like the property on the domain.
            // To use this, place ConvertTwoWay() before ToProperty().

            model.AdminATime.ToProperty(this, _adminA);
            model.AdminBTime.ToProperty(this, _adminB);
            model.NonSalaryTime.ToProperty(this, _nonSalary);
            model.SalaryTime.ToProperty(this, _salary);
            model.TravelTime.ToProperty(this, _travel);
            model.LeaveTime.ToProperty(this, _leave);
            model.TotalTime.ToProperty(this, _total);
        }
    }
}


using PropertyFacadeExample.Domain;
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;

namespace PropertyFacadeExample.ViewModel
{
    public sealed class ChangeTracker : IHasChanges
    {
        private readonly List<IHasChanges> _subjects = new List<IHasChanges>();

        public IReadOnlyValueObservable<bool> HasChanges { get; }
        private readonly BehaviorSubject<bool> _hasChangesSubject = new BehaviorSubject<bool>(default);

        private int TrueCount
        {
            get => _trueCount;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be negative ({value}).");
                }

                _trueCount = value;
            }
        }
        private int _trueCount;

        public ChangeTracker()
        {
            HasChanges = _hasChangesSubject.ToCached();
        }

        public void Add(IHasChanges subject)
        {
            if (subject is null)
            {
                throw new ArgumentNullException(nameof(subject));
            }

            _subjects.Add(subject);

            bool init = true;

            subject.HasChanges.Subscribe(hasChanges =>
            {
                // Do not count no-changes when attaching to the observable.
                if (init && !hasChanges)
                {
                    return;
                }

                TrueCount += hasChanges ? 1 : -1;

                if (TrueCount == 0)
                {
                    _hasChangesSubject.OnNext(false);
                }
                else if (TrueCount == 1 && hasChanges)
                {
                    _hasChangesSubject.OnNext(true);
                }
            });

            init = false;
        }

        public void ResetValue()
        {
            foreach (var facade in _subjects)
            {
                facade.ResetValue();
            }
        }

        public void UpdateOriginalValue()
        {
            foreach (var facade in _subjects)
            {
                facade.UpdateOriginalValue();
            }
        }
    }
}
using System;
using System.Collections.Generic;

namespace PropertyFacadeExample.ViewModel
{
    /// <summary>
    /// Tracks a list of <see cref="IDisposable"/> objects. Prefer this over <see cref="CompositeDisposable"/>
    /// to avoid unnecessary allocations where no items need to be tracked.
    /// </summary>
    public sealed class DisposableTracker
    {
        private List<IDisposable> Disposables { get; set; }

        public void Add(IDisposable disposable)
        {
            if (disposable is null)
            {
                throw new ArgumentNullException(nameof(disposable));
            }

            if (Disposables is null)
            {
                Disposables = new List<IDisposable>();
            }

            Disposables.Add(disposable);
        }

        /// <summary>
        /// Disposes all tracked items and removes them from the list.
        /// </summary>
        public void Dispose()
        {
            if (!(Disposables is null))
            {
                foreach (var disposable in Disposables)
                {
                    disposable.Dispose();
                }

                Disposables.Clear();
            }
        }
    }
}
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace PropertyFacadeExample.ViewModel
{
    public abstract class OneWayFacadeConverter<TSource, TFacade>
    {
        public abstract TFacade SourceToFacade(TSource sourceValue);
    }

    public abstract class TwoWayFacadeConverter<TSource, TFacade> : OneWayFacadeConverter<TSource, TFacade>
    {
        public abstract TSource FacadeToSource(TFacade facadeValue);
    }

    public class ObservableFacadeConverterAdapter<TSource, TFacade> : IObservable<TFacade>
    {
        private readonly IObservable<TSource> _observable;
        private readonly OneWayFacadeConverter<TSource, TFacade> _converter;

        public IDisposable Subscribe(IObserver<TFacade> observer) =>
            _observable
            .Select(sourceValue => _converter.SourceToFacade(sourceValue))
            .Subscribe(observer);

        public ObservableFacadeConverterAdapter(IObservable<TSource> observable, OneWayFacadeConverter<TSource, TFacade> converter)
        {
            _observable = observable ?? throw new ArgumentNullException(nameof(observable));
            _converter = converter ?? throw new ArgumentNullException(nameof(converter));
        }
    }

    public class SubjectFacadeConverterAdapter<TSource, TFacade> : ObservableFacadeConverterAdapter<TSource, TFacade>, ISubject<TFacade>
    {
        private readonly ISubject<TSource> _subject;
        private readonly TwoWayFacadeConverter<TSource, TFacade> _converter;

        public void OnNext(TFacade value) => _subject.OnNext(_converter.FacadeToSource(value));

        public void OnError(Exception error) => _subject.OnError(error);

        public void OnCompleted() => _subject.OnCompleted();

        public SubjectFacadeConverterAdapter(ISubject<TSource> subject, TwoWayFacadeConverter<TSource, TFacade> converter)
            : base(subject, converter)
        {
            _subject = subject ?? throw new ArgumentNullException(nameof(subject));
            _converter = converter ?? throw new ArgumentNullException(nameof(converter));
        }
    }

    public static class FacadeConverterExtensions
    {
        private sealed class OneWayFuncConverter<TSource, TFacade> : OneWayFacadeConverter<TSource, TFacade>
        {
            private readonly Func<TSource, TFacade> _sourceToFacade;

            public override TFacade SourceToFacade(TSource sourceValue) => _sourceToFacade.Invoke(sourceValue);

            public OneWayFuncConverter(Func<TSource, TFacade> sourceToFacade)
            {
                _sourceToFacade = sourceToFacade ?? throw new ArgumentNullException(nameof(sourceToFacade));
            }
        }

        private sealed class TwoWayFuncConverter<TSource, TFacade> : TwoWayFacadeConverter<TSource, TFacade>
        {
            private readonly Func<TSource, TFacade> _sourceToFacade;
            private readonly Func<TFacade, TSource> _facadeToSource;

            public override TFacade SourceToFacade(TSource sourceValue) => _sourceToFacade.Invoke(sourceValue);

            public override TSource FacadeToSource(TFacade facadeValue) => _facadeToSource.Invoke(facadeValue);

            public TwoWayFuncConverter(Func<TSource, TFacade> sourceToFacade, Func<TFacade, TSource> facadeToSource)
            {
                _sourceToFacade = sourceToFacade ?? throw new ArgumentNullException(nameof(sourceToFacade));
                _facadeToSource = facadeToSource ?? throw new ArgumentNullException(nameof(facadeToSource));
            }
        }

        public static SubjectFacadeConverterAdapter<TSource, TFacade> ConvertTwoWay<TSource, TFacade>(
            this ISubject<TSource> source,
            Func<TSource, TFacade> sourceToFacade,
            Func<TFacade, TSource> facadeToSource) =>
                new SubjectFacadeConverterAdapter<TSource, TFacade>(
                    subject: source,
                    converter: new TwoWayFuncConverter<TSource, TFacade>(
                        sourceToFacade: sourceToFacade,
                        facadeToSource: facadeToSource));

        public static ObservableFacadeConverterAdapter<TSource, TFacade> ConvertOneWay<TSource, TFacade>(
            this IObservable<TSource> source,
            Func<TSource, TFacade> sourceToFacade) =>
                new ObservableFacadeConverterAdapter<TSource, TFacade>(
                    observable: source,
                    converter: new OneWayFuncConverter<TSource, TFacade>(
                        sourceToFacade: sourceToFacade));

        public static SubjectFacadeConverterAdapter<TSource, TFacade> ConvertTwoWay<TSource, TFacade>(
            this ISubject<TSource> source,
            TwoWayFacadeConverter<TSource, TFacade> converter) =>
                new SubjectFacadeConverterAdapter<TSource, TFacade>(source, converter);

        public static ObservableFacadeConverterAdapter<TSource, TFacade> ConvertOneWay<TSource, TFacade>(
            this IObservable<TSource> source,
            OneWayFacadeConverter<TSource, TFacade> converter) =>
                new ObservableFacadeConverterAdapter<TSource, TFacade>(source, converter);
    }
}

using PropertyFacadeExample.Domain;
using System;
using System.Reactive.Subjects;

namespace PropertyFacadeExample.ViewModel
{
    /// <summary>
    /// Encapsulates an observable or subject to provide an efficient way to get or set the latest value.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class FacadeValueCache<T> : IObservable<T>, IDisposable
    {
        private IDisposable _disposable;
        private IObservable<T> _observable;
        private Func<T> _getValueFunc;
        private Action<T> _setValueFunc;

        public bool CanSet => !(_setValueFunc is null);

        public bool HasObservable => !(_observable is null);

        public void Attach(IObservable<T> obs)
        {
            if (obs is null)
            {
                throw new ArgumentNullException(nameof(obs));
            }

            Dispose();

            if (obs is IReadOnlyValueObservable<T> iObsVal)
            {
                _observable = iObsVal;
                _getValueFunc = () => iObsVal.Value;
            }
            else if (obs is BehaviorSubject<T> behSub)
            {
                _observable = behSub;
                _getValueFunc = () => behSub.Value;
            }
            else
            {
                var cached = obs.ToCached();

                _observable = cached;
                _disposable = cached;

                _getValueFunc = () => cached.Value;
            }

            if (obs is IObserver<T> iSub)
            {
                _setValueFunc = value => iSub.OnNext(value);
            }
        }

        private void AssertHasObservable()
        {
            if (_observable is null)
            {
                throw new InvalidOperationException("No observable has been attached.");
            }
        }

        public T GetValue()
        {
            AssertHasObservable();

            return _getValueFunc.Invoke();
        }

        public void SetValue(T value)
        {
            AssertHasObservable();

            if (CanSet)
            {
                _setValueFunc.Invoke(value);
            }
            else
            {
                throw new InvalidOperationException("The observable does not implement IObserver.");
            }
        }

        public IDisposable Subscribe(IObserver<T> observer)
        {
            AssertHasObservable();

            return _observable.Subscribe(observer);
        }

        public void Dispose()
        {
            _disposable?.Dispose();
            _observable = null;
            _getValueFunc = null;
            _setValueFunc = null;
        }
    }
}

using PropertyFacadeExample.Domain;

namespace PropertyFacadeExample.ViewModel
{
    public interface IHasChanges
    {
        IReadOnlyValueObservable<bool> HasChanges { get; }
        void ResetValue();
        void UpdateOriginalValue();
    }
}

namespace PropertyFacadeExample.ViewModel
{
    public interface ITracksChanges
    {
        ChangeTracker ChangeTracker { get; }
    }
}

using System.Collections.Generic;
using System;

namespace PropertyFacadeExample
{
    public sealed class NullOrEmptyStringEqualityComparer : EqualityComparer<string>
    {
        public static new NullOrEmptyStringEqualityComparer Default { get; } = new NullOrEmptyStringEqualityComparer(StringComparer.InvariantCulture);

        public IEqualityComparer<string> InnerComparer { get; }

        public NullOrEmptyStringEqualityComparer(IEqualityComparer<string> innerComparer)
        {
            InnerComparer = innerComparer ?? throw new ArgumentNullException(nameof(innerComparer));
        }

        public override bool Equals(string x, string y)
        {
            if (string.IsNullOrEmpty(x) && string.IsNullOrEmpty(y))
            {
                return true;
            }
            else
            {
                return InnerComparer.Equals(x, y);
            }
        }

        public override int GetHashCode(string obj) => InnerComparer.GetHashCode(obj);
    }
}


using PropertyFacadeExample.Domain;
using System;

namespace PropertyFacadeExample.ViewModel
{
    public class PropertyFacade<T> : ReadOnlyPropertyFacade<T>, IHasChanges
    {
        public new T Value
        {
            get => base.Value;
            set
            {
                if (IsDisposed)
                {
                    throw new InvalidOperationException("The subscription for this observable has been disposed. Attach a new subscription by calling Observe.");
                }

                if (_valueCache is null)
                {
                    throw new InvalidOperationException("The observable has not been set yet. Attach a new subscription by calling Observe.");
                }

                _valueCache.SetValue(value);
            }
        }

        public PropertyFacade(UXViewModel viewModel, string propertyName)
            : base(viewModel, propertyName)
        { }

        public PropertyFacade(UXViewModel viewModel, IValueObservable<T> observable, string propertyName)
            : base(viewModel, observable, propertyName)
        { }

        public void ResetValue() => Value = OriginalValue;
    }
}


using PropertyFacadeExample.Domain;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace PropertyFacadeExample.ViewModel
{
    [DebuggerDisplay("(PropertyFacade) {Value}")]
    public class ReadOnlyPropertyFacade<T> : IDisposable
    {
        private readonly UXViewModel _viewModel;
        private readonly string _propertyName;
        protected FacadeValueCache<T> _valueCache;
        private IDisposable _subscription;
        private T _oldValue;

        public bool IsDisposed { get; private set; }

        public T Value => _valueCache.HasObservable ? _valueCache.GetValue() : default;

        public T OriginalValue { get; protected set; }

        public IReadOnlyValueObservable<bool> HasChanges { get; }
        private readonly BehaviorSubject<bool> _hasChangesSubject = new BehaviorSubject<bool>(false);

        public bool SuppressDebugOutput { get; set; }

        public ReadOnlyPropertyFacade(UXViewModel viewModel, string propertyName)
        {
            _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
            _propertyName = propertyName;
            _valueCache = new FacadeValueCache<T>();

            HasChanges = _hasChangesSubject.ToCached();
        }

        public ReadOnlyPropertyFacade(UXViewModel viewModel, IReadOnlyValueObservable<T> observable, string propertyName)
            : this(viewModel, propertyName)
        {
            Observe(observable);
        }

        /// <summary>
        /// Attach the facade to an observable. If an observable is already attached, the subscription is disposed and the new one is attached in its place.
        /// </summary>
        /// <param name="newObservable"></param>
        /// <param name="equalityComparer"></param>
        public void Observe(IObservable<T> newObservable, IEqualityComparer<T> equalityComparer = null)
        {
            if (newObservable is null)
            {
                throw new ArgumentNullException(nameof(newObservable));
            }

            Dispose();

            _valueCache.Attach(newObservable);

            OriginalValue = _valueCache.GetValue();

            DebugMessage("PropertyFacade: New observable mounted on viewmodel='{0}', property='{1}', OriginalValue='{2}'", _viewModel, _propertyName, OriginalValue);

            equalityComparer = equalityComparer ?? GetEqualityComparer<T>();

            IsDisposed = false;

            _subscription = _valueCache.Subscribe(newValue =>
            {
                DebugMessage("PropertyFacade: Received value on viewmodel='{0}', property='{1}', value='{2}'", _viewModel, _propertyName, newValue);

                if (!equalityComparer.Equals(_oldValue, newValue))
                {
                    // Only pump HasChanges if the new HasChanges value is different.

                    bool changed = !equalityComparer.Equals(OriginalValue, newValue);

                    if (_hasChangesSubject.Value != changed)
                    {
                        _hasChangesSubject.OnNext(changed);
                    }

                    RaisePropertyChanged(newValue);
                }
            });
        }

        private IEqualityComparer<TValue> GetEqualityComparer<TValue>()
        {
            if (typeof(TValue) == typeof(string))
            {
                return (IEqualityComparer<TValue>)(IEqualityComparer<string>)NullOrEmptyStringEqualityComparer.Default;
            }
            else
            {
                return EqualityComparer<TValue>.Default;
            }
        }

        private void RaisePropertyChanged(T newValue)
        {
            DebugMessage("PropertyFacade: RaisePropertyChanging: '{0}', property='{1}', old='{2}', new='{3}'", _viewModel, _propertyName, _oldValue, newValue);
            _viewModel.RaisePropertyChanging(_propertyName);

            var oldTemp = _oldValue;
            _oldValue = newValue;

            DebugMessage("PropertyFacade: RaisePropertyChanged: '{0}', property='{1}', old='{2}', new='{3}'", _viewModel, _propertyName, oldTemp, newValue);
            _viewModel.RaisePropertyChanged(_propertyName);
        }

        public void UpdateOriginalValue()
        {
            OriginalValue = Value;

            if (HasChanges.Value)
            {
                _hasChangesSubject.OnNext(false);
            }
        }

        /// <summary>
        /// Disconnects any existing subscription. If none exists, no action is taken.
        /// </summary>
        public void Dispose()
        {
            IsDisposed = true;
            _subscription?.Dispose();
            _valueCache.Dispose();
        }

        [DebuggerStepThrough]
        protected void DebugMessage(string message, params object[] args)
        {
            if (!SuppressDebugOutput && Debugger.IsAttached)
            {
                Debug.WriteLine(message, args);
            }
        }
    }
}

using System.ComponentModel;

namespace PropertyFacadeExample.ViewModel
{
    /// <summary>
    /// A base view model implementation. If we were using ReactiveUI, this would also extend from ReactiveObject.
    /// </summary>
    public abstract class UXViewModel : INotifyPropertyChanged, INotifyPropertyChanging
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public event PropertyChangingEventHandler PropertyChanging;

        public void RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args);
        public void RaisePropertyChanged(string propertyName) => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));

        public void RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args);
        public void RaisePropertyChanging(string propertyName) => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName));

        public DisposableTracker DisposableTracker { get; } = new DisposableTracker();
    }
}


using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace PropertyFacadeExample.ViewModel
{
    public static class UXViewModelExtensions
    {
        public static PropertyFacade<T> PropFacade<T, TViewModel>(this TViewModel vm, Expression<Func<TViewModel, T>> property)
            where TViewModel : UXViewModel
        {
            if (vm is null)
            {
                throw new ArgumentNullException(nameof(vm));
            }

            if (property is null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            var memberExpr = (MemberExpression)property.Body;

            string propName = memberExpr.Member.Name;

            return new PropertyFacade<T>(vm, propName);
        }

        public static ReadOnlyPropertyFacade<T> ReadOnlyPropFacade<T, TViewModel>(this TViewModel vm, Expression<Func<TViewModel, T>> property)
            where TViewModel : UXViewModel
        {
            if (property is null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            var memberExpr = (MemberExpression)property.Body;

            string propName = memberExpr.Member.Name;

            return new ReadOnlyPropertyFacade<T>(vm, propName);
        }

        public static void ToProperty<T>(this IObservable<T> observable, UXViewModel viewModel, ReadOnlyPropertyFacade<T> propertyFacade, IEqualityComparer<T> equalityComparer = null)
        {
            if (observable is null)
            {
                throw new ArgumentNullException(nameof(observable));
            }

            if (viewModel is null)
            {
                throw new ArgumentNullException(nameof(viewModel));
            }

            if (propertyFacade is null)
            {
                throw new ArgumentNullException(nameof(propertyFacade), "The property facade has not been initialized yet.");
            }

            propertyFacade.Observe(observable, equalityComparer);
            viewModel.DisposableTracker.Add(propertyFacade);
        }

        public static T DisposeWith<T>(this T disposable, UXViewModel viewModel) where T : class, IDisposable
        {
            if (disposable is null)
            {
                throw new ArgumentNullException(nameof(disposable));
            }

            if (viewModel is null)
            {
                throw new ArgumentNullException(nameof(viewModel));
            }

            viewModel.DisposableTracker.Add(disposable);

            return disposable;
        }

        public static T TrackChanges<T>(this T subject, ChangeTracker changeTracker)
            where T : class, IHasChanges
        {
            if (subject is null)
            {
                throw new ArgumentNullException(nameof(subject));
            }

            if (changeTracker is null)
            {
                throw new ArgumentNullException(nameof(changeTracker));
            }

            changeTracker.Add(subject);

            return subject;
        }

        public static T TrackChanges<T>(this T subject, ITracksChanges trackerOwner)
            where T : class, IHasChanges
        {
            if (subject is null)
            {
                throw new ArgumentNullException(nameof(subject));
            }

            if (trackerOwner is null)
            {
                throw new ArgumentNullException(nameof(trackerOwner));
            }

            return TrackChanges(subject, trackerOwner.ChangeTracker);
        }
    }
}

using System;
using System.Diagnostics;
using System.Reactive;

namespace PropertyFacadeExample.Domain
{
    public interface ICachedObservable<T> : IReadOnlyValueObservable<T>
    {
        bool HasValue { get; }
    }

    /// <summary>
    /// Represents an observable that caches the latest value of another observable.
    /// This observable has the same subscription semantics as ReplaySubject(1): if there is a value present, the value will replay when a subscription is made.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [DebuggerDisplay("(CachedObservable) {Value}")]
    public sealed class CachedObservable<T> : ObservableBase<T>, ICachedObservable<T>
    {
        private readonly IDisposable _registration;
        private readonly IObservable<T> _source;

        public bool HasValue { get; private set; }

        public T Value { get; private set; }

        public CachedObservable(IObservable<T> source)
        {
            _source = source ?? throw new ArgumentNullException(nameof(source));

            _registration = _source.Subscribe(x =>
            {
                HasValue = true;
                Value = x;
            });
        }

        public void Dispose() => _registration.Dispose();

        protected override IDisposable SubscribeCore(IObserver<T> observer)
        {
            if (observer is null)
            {
                throw new ArgumentNullException(nameof(observer));
            }

            var sub = _source.SubscribeSafe(observer);

            if (HasValue)
            {
                observer.OnNext(Value);
            }

            return sub;
        }

        public override string ToString() => $"(CachedObservable) {Value}";
    }

    public static class CachedObservableExtensions
    {
        public static ICachedObservable<T> ToCached<T>(this IObservable<T> source) => new CachedObservable<T>(source);
    }
}

using System;

namespace PropertyFacadeExample.Domain
{
    internal static class RxDomain
    {
        public static IValueObservable<T> ObservableProperty<T>()
            => new ValueObservable<T>();

        public static IValueObservable<T> ObservableProperty<T>(T defaultValue, CoerceValueHandler<T> coerceValue = null)
            => new ValueObservable<T>(defaultValue, coerceValue);

        public static IValueObservable<T> ObservableProperty<T>(T defaultValue, CoerceValueHandler<T> coerceValue, out IDisposable coercerDelayToken)
        {
            var obs = new ValueObservable<T>(defaultValue, coerceValue, delayCoercer: true);
            coercerDelayToken = obs.GetCoercerDelayToken();
            return obs;
        }

        public static IValueObservable<T> ObservableProperty<T>(CoerceValueHandler<T> coerceValue)
            => new ValueObservable<T>(default, coerceValue);

        public static IReadOnlyValueObservable<T> ReadOnlyConstant<T>(T value)
            => new ReadOnlyConstantValueObservable<T>(value);
    }
}

using System;
using System.Diagnostics;
using System.Reactive.Disposables;
using System.Reactive.Subjects;

namespace PropertyFacadeExample.Domain
{

    public interface IReadOnlyValueObservable<T> : IObservable<T>, IDisposable
    {
        T Value { get; }
    }

    public interface IValueObservable<T> : IReadOnlyValueObservable<T>, ISubject<T>
    {
        new T Value { get; set; }
    }

    /// <summary>
    /// Same thing as Observable.Return() except as a <see cref="IReadOnlyValueObservable{T}"/>.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class ReadOnlyConstantValueObservable<T> : IReadOnlyValueObservable<T>
    {
        public T Value { get; }

        public ReadOnlyConstantValueObservable(T value) => Value = value;

        public void Dispose()
        { }

        public IDisposable Subscribe(IObserver<T> observer)
        {
            if (observer is null)
            {
                throw new ArgumentNullException(nameof(observer));
            }

            observer.OnNext(Value);

            return Disposable.Empty;
        }
    }

    public delegate T CoerceValueHandler<T>(T oldValue, T newValue);

    /// <summary>
    /// Represents a reactive subject that caches the latest value. It exhibits the same behavior as
    /// <see cref="BehaviorSubject{T}"/> except it has a settable <see cref="Value"/> property.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [DebuggerDisplay("(ValueObservable) {Value}")]
    public sealed class ValueObservable<T> : IValueObservable<T>
    {
        private readonly BehaviorSubject<T> _subject;
        private readonly CoerceValueHandler<T> _coerceValue;

        public ValueObservable(T initialValue = default, CoerceValueHandler<T> coerceValue = default, bool delayCoercer = false)
        {
            _coerceValue = coerceValue ?? ((_, newValue) => newValue);
            _subject = new BehaviorSubject<T>(delayCoercer ? initialValue : _coerceValue.Invoke(default, initialValue));
        }

        public T Value
        {
            get => _subject.Value;
            set => OnNext(value);
        }

        public IDisposable Subscribe(IObserver<T> observer) => _subject.SubscribeSafe(observer);

        public void OnCompleted() => _subject.OnCompleted();

        public void OnError(Exception error) => _subject.OnError(error);

        public void OnNext(T newValue) => _subject.OnNext(_coerceValue.Invoke(Value, newValue));

        public void Dispose() => _subject.Dispose();

        public override string ToString() => $"(ValueObservable<{typeof(T).Name}>) {Value}";

        public void CoerceCurrent() => _coerceValue.Invoke(Value, Value);

        public IDisposable GetCoercerDelayToken() => Disposable.Create(() => CoerceCurrent());
    }

    /// <summary>
    /// Allows you to build custom observer logic for complex coercion scenarios.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class ValueObservableFacade<T> : IValueObservable<T>
    {
        private readonly IObservable<T> _observable;
        private readonly IObserver<T> _observer;
        private readonly Func<T> _getValue;
        private readonly Action<T> _setValue;

        public ValueObservableFacade(IObservable<T> observable, IObserver<T> observer, Func<T> getValue, Action<T> setValue = null)
        {
            _observable = observable ?? throw new ArgumentNullException(nameof(observable));
            _observer = observer ?? throw new ArgumentNullException(nameof(observer));
            _getValue = getValue ?? throw new ArgumentNullException(nameof(getValue));
            _setValue = setValue;

            _observable.Subscribe(next => Value = next);
        }

        /// <summary>
        /// Gets or sets the value. If the value is the same as the one stored, observers will not be notified. Use <see cref="OnNext(T)"/> to override this.
        /// </summary>
        public T Value
        {
            get => _getValue();
            set
            {
                if (!Equals(value, _getValue()))
                {
                    OnNext(value);
                }
            }
        }

        public void Dispose()
        {
            if (_observer is IDisposable d)
            {
                d.Dispose();
            }
        }

        public void OnCompleted() => _observer.OnCompleted();

        public void OnError(Exception error) => _observer.OnError(error);

        public void OnNext(T value)
        {
            _setValue?.Invoke(value);
            _observer.OnNext(value);
        }

        public IDisposable Subscribe(IObserver<T> observer) => _observable.SubscribeSafe(observer);
    }
}

using System.Linq;
using System.Reactive.Linq;
using static PropertyFacadeExample.Domain.RxDomain;

namespace PropertyFacadeExample.Domain.Models
{
    /// <summary>
    /// An example domain model that encapsulates working time in hours.
    /// </summary>
    public sealed class WorkingAttendance
    {
        public IValueObservable<decimal> AdminATime { get; }
        public IValueObservable<decimal> AdminBTime { get; }
        public IValueObservable<decimal> NonSalaryTime { get; }
        public IValueObservable<decimal> SalaryTime { get; }
        public IValueObservable<decimal> TravelTime { get; }
        public IValueObservable<decimal> LeaveTime { get; }

        public IReadOnlyValueObservable<decimal> TotalTime { get; }

        public WorkingAttendance(
            decimal administrativeATime,
            decimal administrativeBTime,
            decimal nonSalaryTime,
            decimal salaryTime,
            decimal travelTime,
            decimal leaveTime)
        {
            AdminATime = ObservableProperty(administrativeATime, ClipNegativeHoursToZero);
            AdminBTime = ObservableProperty(administrativeBTime, ClipNegativeHoursToZero);
            NonSalaryTime = ObservableProperty(nonSalaryTime);
            SalaryTime = ObservableProperty(salaryTime);
            TravelTime = ObservableProperty(travelTime);
            LeaveTime = ObservableProperty(leaveTime, ClipNegativeHoursToZero);

            TotalTime = Observable.CombineLatest(
                AdminATime,
                AdminBTime,
                NonSalaryTime,
                SalaryTime,
                TravelTime,
                LeaveTime)
                .Select(hours => hours.Sum())
                .ToCached();
        }

        private static decimal ClipNegativeHoursToZero(decimal _, decimal newTime) => newTime < 0 ? 0 : newTime;
    }
}