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);
}
@mausch
Copy link

mausch commented Apr 16, 2013

Cool, but I'd rather use this approach: http://v2matveev.blogspot.com/2010/05/copy-and-update-in-c.html

@bradphelan
Copy link
Author

No way. That's a lot of boiler plate and a lot of code to get wrong. My solution just requires tagging with Immutable and all properties are Copy and Update compatible immediately.

@bradphelan
Copy link
Author

The sort of typical cut and paste errors that this kind of code generates are things like this.

return new DataObject
   {
       Id = id.ValueOrDefault(Id),
       Value = value.ValueOrDefault(Id),
       Object = obj.ValueOrDefault(Object)
   };

Note the Id on the right hand side of the Value assignment. Easy
to do cause you copy the above line and forget to change the
name in both places. If the types are the same then you get
no compiler error and you then have to write tests for this
behaviour which will probably generate copy paste errors.

@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