Skip to content

Instantly share code, notes, and snippets.

@fdbeirao
Created May 31, 2019 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fdbeirao/2b07ac4994d214d9bb35089a83532434 to your computer and use it in GitHub Desktop.
Save fdbeirao/2b07ac4994d214d9bb35089a83532434 to your computer and use it in GitHub Desktop.
C# IObservableProperty
using System;
namespace ObservableProperty
{
public interface IObservableProperty<T> : IObservable<T>
{
T Value { get; }
}
}
using System;
using System.Reactive.Subjects;
namespace ObservableProperty
{
/// <summary>
/// ObservableProperty is an immutable data structure which holds the last/current value of a property,
/// as well as providing you with an observable stream of changes to this property. The property observers
/// will be carried over even with "mutations" of this immutable data structure.
/// </summary>
public sealed class ObservableProperty<T> : IObservableProperty<T>, IObservable<T>, IDisposable
{
/// <summary>
/// Provides a new instance of the ObservableProperty.
/// </summary>
public static ObservableProperty<T> New(T initialValue) =>
new ObservableProperty<T>(initialValue, new BehaviorSubject<T>(initialValue));
/// <summary>
/// This is how you "mutate" this observable property. Keep in mind that you receive a new instance of
/// this immutable data structure, so you have to hold it. Any subscribers of notifications will be kept
/// and notified of this modification.
/// </summary>
public ObservableProperty<T> WithValue(T newValue)
{
var updatedProperty = new ObservableProperty<T>(newValue, _stream);
updatedProperty._stream.OnNext(newValue);
return updatedProperty;
}
private ObservableProperty(T value, ISubject<T> stream) =>
(Value, _stream) =
(value, @stream);
/// <summary>
/// The last/current value assigned to this property.
/// </summary>
public T Value { get; }
/// <summary>
/// Subscribe to future modifications of this property. Upon subscription you will also receive the
/// current value of the property. As per the IObservable<T> pattern, dispose of the returned object
/// to cancel this subscription.
/// </summary>
public IDisposable Subscribe(IObserver<T> observer) =>
_stream.Subscribe(observer);
private readonly ISubject<T> _stream;
#region IDisposable Support
private bool hasBeenDisposed = false; // To detect redundant calls
public void Dispose() =>
Dispose(true);
private void Dispose(bool disposing)
{
if (!hasBeenDisposed)
{
if (disposing)
{
_stream.OnCompleted();
}
hasBeenDisposed = true;
}
}
#endregion
}
/// <summary>
/// ObservableProperty is an immutable data structure which holds the last/current value of a property,
/// as well as providing you with an observable stream of changes to this property. The property observers
/// will be carried over even with "mutations" of this immutable data structure.
/// </summary>
public static class ObservableProperty
{
public static ObservableProperty<T> New<T>(T initialValue) =>
ObservableProperty<T>.New(initialValue);
}
}
using ObservableProperty;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Xunit;
namespace ObservablePropertyTests
{
public class Tests
{
public class User
{
private readonly ObservableProperty<string> _name;
public string Id { get; }
public string Name => _name.Value;
public IObservable<string> NameStream => _name;
private User(string id, ObservableProperty<string> name) =>
(Id, _name) =
(id, name);
public static User New(string id, string name) =>
new User(id, ObservableProperty<string>.New(name));
public User WithName(string name) =>
new User(Id, _name.WithValue(name));
}
private static string SomeRandomString =>
Guid.NewGuid().ToString();
[Fact]
public async Task OnCreation_Then_Subscription_WeGetCurrentValue()
{
var name = SomeRandomString;
var user1 = User.New(id: SomeRandomString, name);
Assert.Equal(name, user1.Name);
var nameInStream = await user1.NameStream.Take(1);
Assert.Equal(name, nameInStream);
}
[Fact]
public async Task OnModification_Subscription_CarriesOver()
{
var name = SomeRandomString;
var user1 = User.New(id: SomeRandomString, name);
var initialNameInStream = await user1.NameStream.Take(1);
Assert.Equal(name, initialNameInStream);
var user2 = user1.WithName("new name");
var newUserNameInStream = await user1.NameStream.Take(1);
Assert.Equal("new name", newUserNameInStream);
}
[Fact]
public async Task Subscription_HasOnlyMostRecentValue()
{
var user1 = User.New(id: SomeRandomString, SomeRandomString);
var newUser = user1.WithName("new name");
var newUserNameInStream = await user1.NameStream.Take(1);
Assert.Equal("new name", newUserNameInStream);
}
}
}
@fdbeirao
Copy link
Author

I still have mixed feelings about this implementation. The fact that ObservableProperty is an immutable data structure signals some safety guarantees, but then it has an observable side-effect on execution (invoking onNext on the IObservable).. maybe this could be useful for someone, but use it with a grain of salt (like all code).
Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment