Skip to content

Instantly share code, notes, and snippets.

@bradphelan
Last active December 16, 2015 06:59
Show Gist options
  • Save bradphelan/5395652 to your computer and use it in GitHub Desktop.
Save bradphelan/5395652 to your computer and use it in GitHub Desktop.
Immutable persistent object pattern in C#. We want to be able to set a property at an arbitrary depth and return a new tree without changing the old tree. This is possible with a few reflection tricks. I have a very specific use case for this and it is to build an undo stack for my application without a messy command pattern system.
[Fact]
public void UndoStackChangeNotificationSpec()
{
var stack = new UndoStack<A>(new A(10, null));
var psubj = stack.ImmutablePropertySubject(x => x.P);
int current = 0;
psubj.Subscribe(c => current = c);
psubj.OnNext(20);
stack.Current.P.Should().Be(20);
current.Should().Be(20);
psubj.OnNext(30);
stack.Current.P.Should().Be(30);
current.Should().Be(30);
psubj.OnNext(40);
stack.Current.P.Should().Be(40);
current.Should().Be(40);
stack.Undo();
stack.Current.P.Should().Be(30);
current.Should().Be(30);
stack.Undo();
stack.Current.P.Should().Be(20);
current.Should().Be(20);
stack.Undo();
stack.Current.P.Should().Be(10);
current.Should().Be(10);
// Verify we don't pop the initial value
stack.Undo();
stack.Current.Should().NotBe(null);
current.Should().Be(10);
}
interface Immutable { };
static class ImmutableExtension
{
public static T Set<T,V>
(this T This, Expression<Func<T, V>> prop, V value)
where T : class, Immutable
{
var names = ReactiveUI.Reflection.ExpressionToPropertyNames(prop).ToList();
return This.Set(names, value);
}
public static T Set<T,V>
(this T This, List<string> names, V value)
where T : class, Immutable
{
var name = names.First();
var rest = names.Skip(1).ToList();
if (names.Count==1)
{
var copy = This.ShallowClone();
copy.SetPrivatePropertyValue(names.First(), value);
return copy as T;
}else{
var copy = This.ShallowClone();
var subtree = copy
.GetPrivatePropertyValue<Immutable>(name)
.Set(rest, value);
copy.SetPrivatePropertyValue(names.First(), subtree );
return copy as T;
}
}
public static object ShallowClone(this object o)
{
var flags = BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
return o.GetType().InvokeMember("MemberwiseClone", flags, null, o, null);
}
/// <summary>
/// Sets a _private_ Property Value from a given Object. Uses Reflection.
/// Throws a ArgumentOutOfRangeException if the Property is not found.
/// </summary>
/// <typeparam name="T">Type of the Property</typeparam>
/// <param name="obj">Object from where the Property Value is set</param>
/// <param name="propName">Propertyname as string.</param>
/// <param name="val">Value to set.</param>
/// <returns>PropertyValue</returns>
public static void SetPrivatePropertyValue<T>(this object obj, string propName, T val)
{
Type t = obj.GetType();
if (t.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) == null)
throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName));
t.InvokeMember(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, obj, new object[] { val });
}
/// <summary>
/// Returns a _private_ Property Value from a given Object. Uses Reflection.
/// Throws a ArgumentOutOfRangeException if the Property is not found.
/// </summary>
/// <typeparam name="T">Type of the Property</typeparam>
/// <param name="obj">Object from where the Property Value is returned</param>
/// <param name="propName">Propertyname as string.</param>
/// <returns>PropertyValue</returns>
public static T GetPrivatePropertyValue<T>(this object obj, string propName)
{
if (obj == null) throw new ArgumentNullException("obj");
PropertyInfo pi = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (pi == null) throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}", propName, obj.GetType().FullName));
return (T)pi.GetValue(obj, null);
}
}
class A : Immutable
{
public int P { get; private set; }
public B B { get; private set; }
public A(int p, B b)
{
P = p;
B = b;
}
}
class B : Immutable
{
public int P { get; private set; }
public C C { get; private set; }
public B(int p, C c)
{
P = p;
C = c;
}
}
class C : Immutable
{
public int P { get; private set; }
public C(int p)
{
P = p;
}
}
namespace Utils.Spec
{
public class ImmutableObjectPatternSpec
{
[Fact]
public void OhMyLord()
{
var a = new A
( p:10
, b: new B
( p: 20
, c : new C(30)));
var a_ = a.Set(p => p.B.C.P, 10);
a.Should().NotBe(a_);
a.B.C.P.Should().Be(30);
a_.B.C.P.Should().Be(10);
}
}
}
[Fact]
public void UndoStackLensSpec()
{
var stack0 = new UndoStack<A>(new A(1, new B(1,new C(5))));
// Create a lens onto A.B a lens is ISubject<T> but
// also remembers the property path it was generated
// from
var bLens = stack0.ImmutablePropertySubject(x => x.B);
// Create a sub lens to A.B.C.P
var bp = bLens.ForProperty(x => x.C.P);
// Change the property
bp.OnNext(10);
// Verify that it is changed in the undo stack
stack0.Current.B.C.P.Should().Be(10);
stack0.Undo();
// Verify that undo still works
stack0.Current.B.C.P.Should().Be(5);
}
class UndoStack<T>
where T : class, Immutable
{
private Stack<T> ChangeHistory = new Stack<T>();
public T Set<V>
(Expression<Func<T, V>> prop, V value)
{
var last = ChangeHistory.Peek();
var next = last.Set(prop, value);
ChangeHistory.Push(next);
return next;
}
public T Undo()
{
ChangeHistory.Pop();
return ChangeHistory.Peek();
}
public UndoStack(T initial)
{
ChangeHistory.Push(initial);
}
}
[Fact]
public void UndoStackSpec()
{
var stack = new UndoStack<A>(new A(10, null));
stack.Current().B.Should().Be(null);
stack.Set(x => x.B, new B(20, null));
stack.Current().B.Should().NotBe(null);
stack.Current().B.P.Should().Be(20);
stack.Undo();
stack.Current().B.Should().Be(null);
}
@anaisbetts
Copy link

You should read through the old ReactiveUI.Serialization code, it did something similar to implement object persistence a-la Google Wave, where you could traverse object "history"

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