Last active
October 11, 2016 14:26
-
-
Save ig-sinicyn/b4cc36175a002f6c08f2eeb9f35faea2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using Xunit; | |
using static Xunit.Assert; | |
namespace DepPropertySample | |
{ | |
public class Program | |
{ | |
static void Main(string[] args) => UseCase(); | |
[Fact] | |
public static void UseCase() | |
{ | |
var a = new Config() | |
{ | |
Title = "Hello", | |
RepeatCount = 123, | |
AdvancedConfig = new DerivedConfig() | |
{ | |
RepeatCount = 11 | |
} | |
}; | |
// 1.1. update unfrozen | |
a.Title += ", world!!!"; | |
var msg = Throws<InvalidOperationException>(() => a.AdvancedConfig.AdvancedConfig = a.AdvancedConfig); | |
Equal(msg.Message, "Circular references are not allowed."); | |
a.AdvancedConfig.AdvancedConfig = new DerivedConfig(); | |
// 1.2. Check properties | |
Equal(a.Frozen, false); | |
Equal(a.AdvancedConfig.Frozen, false); | |
Equal(a.AdvancedConfig.AdvancedConfig.Frozen, false); | |
Equal(a.RepeatCount, 123); | |
Equal(a.Title, "Hello, world!!!"); | |
Equal(a.AdvancedConfig.RepeatCount, 11); // overriden | |
Equal(a.AdvancedConfig.LaunchCount, 111); // fallback value | |
Equal(a.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.AdvancedConfig.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.Parent, null); | |
Equal(a.AdvancedConfig.Parent, a); | |
Equal(a.AdvancedConfig.AdvancedConfig.Parent, a.AdvancedConfig); | |
True(a.HasValue(Config.RepeatCountProperty)); | |
True(a.HasValue(Config.TitleProperty)); | |
True(a.AdvancedConfig.HasValue(Config.RepeatCountProperty)); | |
False(a.AdvancedConfig.HasValue(DerivedConfig.LaunchCountProperty)); | |
False(a.AdvancedConfig.HasValue(Config.TitleProperty)); | |
False(a.AdvancedConfig.AdvancedConfig.HasValue(Config.TitleProperty)); | |
// 2.1. Freeze and try update | |
a.Freeze(); | |
Throws<InvalidOperationException>(() => a.RepeatCount++); | |
Throws<InvalidOperationException>(() => a.AdvancedConfig.RepeatCount++); | |
// 2.2. Check properties | |
Equal(a.Frozen, true); | |
Equal(a.AdvancedConfig.Frozen, true); | |
Equal(a.AdvancedConfig.AdvancedConfig.Frozen, true); | |
Equal(a.RepeatCount, 123); | |
Equal(a.Title, "Hello, world!!!"); | |
Equal(a.AdvancedConfig.RepeatCount, 11); | |
Equal(a.AdvancedConfig.LaunchCount, 111); // fallback value | |
Equal(a.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.AdvancedConfig.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.Parent, null); | |
Equal(a.AdvancedConfig.Parent, a); | |
Equal(a.AdvancedConfig.AdvancedConfig.Parent, a.AdvancedConfig); | |
True(a.HasValue(Config.RepeatCountProperty)); | |
True(a.HasValue(Config.TitleProperty)); | |
True(a.AdvancedConfig.HasValue(Config.RepeatCountProperty)); | |
False(a.AdvancedConfig.HasValue(DerivedConfig.LaunchCountProperty)); | |
False(a.AdvancedConfig.HasValue(Config.TitleProperty)); | |
False(a.AdvancedConfig.AdvancedConfig.HasValue(Config.TitleProperty)); | |
// 3.1. Unfreeze | |
var aOld = a; | |
a = a.UnfreezeCopy(); | |
NotEqual(a, aOld); | |
// 3.2. Check properties | |
Equal(a.Frozen, false); | |
Equal(a.AdvancedConfig.Frozen, false); | |
Equal(a.AdvancedConfig.AdvancedConfig.Frozen, false); | |
Equal(a.RepeatCount, 123); | |
Equal(a.Title, "Hello, world!!!"); | |
Equal(a.AdvancedConfig.RepeatCount, 11); | |
Equal(a.AdvancedConfig.LaunchCount, 111); // fallback value | |
Equal(a.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.AdvancedConfig.AdvancedConfig.Title, "Hello, world!!!"); // from parent | |
Equal(a.Parent, null); | |
Equal(a.AdvancedConfig.Parent, a); | |
Equal(a.AdvancedConfig.AdvancedConfig.Parent, a.AdvancedConfig); | |
True(a.HasValue(Config.RepeatCountProperty)); | |
True(a.HasValue(Config.TitleProperty)); | |
True(a.AdvancedConfig.HasValue(Config.RepeatCountProperty)); | |
False(a.AdvancedConfig.HasValue(DerivedConfig.LaunchCountProperty)); | |
False(a.AdvancedConfig.HasValue(Config.TitleProperty)); | |
False(a.AdvancedConfig.AdvancedConfig.HasValue(Config.TitleProperty)); | |
// 4.1. Update unfrozen | |
a.RepeatCount++; | |
a.AdvancedConfig.RepeatCount++; | |
a.AdvancedConfig.Title = "Hello, world V2!!!"; | |
// 4.2. Check properties | |
Equal(a.Frozen, false); | |
Equal(a.AdvancedConfig.Frozen, false); | |
Equal(a.RepeatCount, 124); | |
Equal(a.Title, "Hello, world!!!"); | |
Equal(a.AdvancedConfig.RepeatCount, 12); | |
Equal(a.AdvancedConfig.AdvancedConfig.RepeatCount, 12); // from parent | |
Equal(a.AdvancedConfig.Title, "Hello, world V2!!!"); // overriden | |
Equal(a.AdvancedConfig.AdvancedConfig.Title, "Hello, world V2!!!"); // from parent | |
True(a.HasValue(Config.RepeatCountProperty)); | |
Equal(a.Parent, null); | |
Equal(a.AdvancedConfig.Parent, a); | |
Equal(a.AdvancedConfig.AdvancedConfig.Parent, a.AdvancedConfig); | |
True(a.HasValue(Config.TitleProperty)); | |
True(a.AdvancedConfig.HasValue(Config.RepeatCountProperty)); | |
False(a.AdvancedConfig.HasValue(DerivedConfig.LaunchCountProperty)); | |
True(a.AdvancedConfig.HasValue(Config.TitleProperty)); | |
False(a.AdvancedConfig.AdvancedConfig.HasValue(Config.TitleProperty)); | |
} | |
} | |
public class Config : DepObject | |
{ | |
public static readonly DepPropertyKey<int> RepeatCountProperty = DepPropertyKey.Create((Config r) => r.RepeatCount); | |
public static readonly DepPropertyKey<string> TitleProperty = | |
DepPropertyKey.Create((Config r) => r.Title); | |
public static readonly DepPropertyKey<DerivedConfig> AdvancedConfigProperty = | |
DepPropertyKey.Create((Config r) => r.AdvancedConfig); | |
public new Config UnfreezeCopy() => UnfreezeCopyCore(this); | |
public int RepeatCount | |
{ | |
get { return RepeatCountProperty[this]; } | |
set { RepeatCountProperty[this] = value; } | |
} | |
public string Title | |
{ | |
get { return TitleProperty[this]; } | |
set { TitleProperty[this] = value; } | |
} | |
public DerivedConfig AdvancedConfig | |
{ | |
get { return AdvancedConfigProperty[this]; } | |
set { AdvancedConfigProperty[this] = value; } | |
} | |
} | |
public class DerivedConfig : Config | |
{ | |
public static readonly DepPropertyKey<int?> LaunchCountProperty = DepPropertyKey.Create( | |
(DerivedConfig r) => r.LaunchCount, 111); | |
public new DerivedConfig UnfreezeCopy() => UnfreezeCopyCore(this); | |
public int? LaunchCount | |
{ | |
get { return LaunchCountProperty[this]; } | |
set { LaunchCountProperty[this] = value; } | |
} | |
} | |
public abstract class DepPropertyKey | |
{ | |
public static readonly object EmptyValue = new object(); | |
public static DepPropertyKey<T> Create<T>(string name) => new DepPropertyKey<T>(name); | |
public static DepPropertyKey<T> Create<T>(string name, T fallbackValue) => new DepPropertyKey<T>(name, fallbackValue); | |
public static DepPropertyKey<T> Create<TOwner, T>(Expression<Func<TOwner, T>> name) => | |
new DepPropertyKey<T>(((MemberExpression)name.Body).Member.Name); | |
public static DepPropertyKey<T> Create<TOwner, T>(Expression<Func<TOwner, T>> name, T fallbackValue) => | |
new DepPropertyKey<T>(((MemberExpression)name.Body).Member.Name, fallbackValue); | |
protected DepPropertyKey(string name, object fallbackValue) | |
{ | |
Name = name; | |
FallbackValue = fallbackValue; | |
} | |
public string Name { get; } | |
public object FallbackValue { get; } | |
public object this[DepObject obj] | |
{ | |
get { return obj.GetValue(this); } | |
set { obj.SetValue(this, value); } | |
} | |
public bool HasValue(DepObject obj) => obj.HasValue(this); | |
public void ClearValue(DepObject obj) => this[obj] = EmptyValue; | |
} | |
public class DepPropertyKey<T> : DepPropertyKey | |
{ | |
public DepPropertyKey(string name) : base(name, default(T)) { } | |
public DepPropertyKey(string name, T fallbackValue) : base(name, fallbackValue) | |
{ | |
FallbackValue = fallbackValue; | |
} | |
public new T FallbackValue { get; } | |
public new T this[DepObject obj] | |
{ | |
get { return obj.GetValue(this); } | |
set { obj.SetValue(this, value); } | |
} | |
} | |
public class DepObject | |
{ | |
// TODO: cache empty dict instance & compare&swap on mutate. | |
private readonly Dictionary<DepPropertyKey, object> values = | |
new Dictionary<DepPropertyKey, object>(); | |
private DepObject m_Parent; | |
private void With(DepObject parent, Dictionary<DepPropertyKey, object> newValues) | |
{ | |
if (Frozen || Parent != null || values.Any()) | |
throw new InvalidOperationException(); | |
Parent = parent; | |
foreach (var newValue in newValues) | |
{ | |
values.Add(newValue.Key, newValue.Value); | |
} | |
} | |
public bool Frozen { get; private set; } | |
internal DepObject Parent | |
{ | |
get { return m_Parent; } | |
set | |
{ | |
if (Frozen) | |
throw new InvalidOperationException(); | |
m_Parent = value; | |
} | |
} | |
internal object GetValue(DepPropertyKey key) | |
{ | |
DepObject current = this; | |
while (current != null) | |
{ | |
object result; | |
if (current.values.TryGetValue(key, out result)) | |
{ | |
return object.ReferenceEquals(result, DepPropertyKey.EmptyValue) | |
? key.FallbackValue | |
: result; | |
} | |
current = current.Parent; | |
} | |
return key.FallbackValue; | |
} | |
public bool HasValue(DepPropertyKey key) | |
{ | |
object result; | |
if (values.TryGetValue(key, out result)) | |
{ | |
return !object.ReferenceEquals(result, DepPropertyKey.EmptyValue); | |
} | |
return false; | |
} | |
internal T GetValue<T>(DepPropertyKey<T> key) | |
{ | |
return (T)GetValue((DepPropertyKey)key); | |
} | |
internal void SetValue(DepPropertyKey key, object value) | |
{ | |
if (Frozen) | |
throw new InvalidOperationException("Frozen."); | |
var depObj = value as DepObject; | |
if (depObj != null) | |
{ | |
var current = this; | |
while (current != null) | |
{ | |
if (object.ReferenceEquals(current, depObj)) | |
throw new InvalidOperationException("Circular references are not allowed."); | |
current = current.Parent; | |
} | |
depObj.Parent = this; | |
} | |
values[key] = value; | |
} | |
internal void SetValue<T>(DepPropertyKey<T> key, T value) => SetValue((DepPropertyKey)key, value); | |
public void Freeze() | |
{ | |
if (Frozen) | |
return; | |
Frozen = true; | |
foreach (var details in values.Values.OfType<DepObject>()) | |
{ | |
details.Freeze(); | |
} | |
} | |
public DepObject UnfreezeCopy() => UnfreezeCopyCore(this); | |
protected static T UnfreezeCopyCore<T>(T instance) | |
where T : DepObject => | |
(T)UnfreezeCopyCore(instance, new Dictionary<DepObject, DepObject>()); | |
private static DepObject UnfreezeCopyCore( | |
DepObject instance, | |
Dictionary<DepObject, DepObject> unfrozen) | |
{ | |
if (instance == null) | |
return null; | |
DepObject unfrozenInst; | |
if (unfrozen.TryGetValue(instance, out unfrozenInst)) | |
throw new InvalidOperationException("Circular reference detected!"); | |
// TODO: emit .ctor delegate | |
var result = (DepObject)Activator.CreateInstance(instance.GetType()); | |
unfrozen.Add(instance, result); | |
var newValues = new Dictionary<DepPropertyKey, object>(); | |
foreach (var valuePair in instance.values) | |
{ | |
var depObj = valuePair.Value as DepObject; | |
if (depObj == null) | |
{ | |
newValues.Add(valuePair.Key, valuePair.Value); | |
} | |
else | |
{ | |
newValues.Add(valuePair.Key, UnfreezeCopyCore(depObj, unfrozen)); | |
} | |
} | |
result.With( | |
instance.Parent == null ? null : unfrozen[instance.Parent], | |
newValues); | |
return result; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment