I am doing some math - combining, reusing, extending different analyses methods over the same original data set. It would be reasonable to use C# extension methods in my case instead of defining everything in one class. The same time there are too much stuff to be constantly recalculated in this extension methods - I definitely need something like extension write-once/calculated-once extension property mechanism. Here is how it looks like and how to consume it. Please comment on this if anything smells for you (sources are on GitHub).
Let's imagine that we have class Person
:
namespace People
{
class Person
{
}
}
To extend it with couple properties:
Name
- write-once extension propertySalutation
- calculated-once extension property
We define:
namespace HumanResource
{
using People;
using ExtensionProperties;
static class ContactInfoProperties
{
static readonly ExtensionProperty<string> NameProperty =
new ExtensionProperty<string>();
static readonly ExtensionProperty<string> SalutationProperty =
new ExtensionProperty<string>();
public static string Name(this Person person, string name) =>
person.Add(NameProperty, name);
public static string Name(this Person person) =>
person.Get(NameProperty);
public static string Salutation(this Person person) =>
person.GetOrAdd(SalutationProperty, () => "Hello " + person.Name());
}
}
where ExtensionProperty<T>
and three extension methods Add
, Get
, GetOrAdd
are being imported from ExtensionProperties
namespace.
We define ExtensionProperty<T>
fields to uniquely identify property identity and type for the storage system.
Add
method adds property value to the storage. It throws RedefinedPropertyException
if value has been already associated with the property.
Get
method tries to read value from the storage or throws UndefinedPropertyException
if values is missing.
GetOrAdd
method always succeeds by attempting to return existing value. It calculates, persists and returns new value if missing.
We can use it this way:
var person = new Person();
person.Name("John Doe");
Console.WriteLine("Name: " + person.Name());
Console.WriteLine("Salutation: " + person.Salutation());
OK, the `ExtensionProperties' lib itself comes below.
ExtensionProperty<T>
definition:
public interface IExtensionProperty
{
string Name { get; }
}
public class ExtensionProperty<T> : IExtensionProperty
{
public ExtensionProperty([CallerMemberName] string name = null)
{
Name = name ?? "UnknownProperty";
}
public string Name { get; }
public override string ToString() => Name;
}
Add
, Get
, GetOrAdd
extension methods comes from:
public static class ExtensionProperty
{
static ConditionalWeakTable<object, ValueStore> Table { get; } =
new ConditionalWeakTable<object, ValueStore>();
static ValueStore Store(object context) =>
Table.GetOrCreateValue(context);
public static T Add<T>(
this object context,
ExtensionProperty<T> property,
T value) =>
Store(context)
.Add(property, value);
public static T Get<T>(
this object context,
ExtensionProperty<T> property) =>
Store(context)
.Get(property);
public static T GetOrAdd<T>(
this object context,
ExtensionProperty<T> property,
Func<T> valueFactory) =>
Store(context)
.GetOrAdd(property, valueFactory);
}
Where ValueStore
are:
class ValueStore
{
ConcurrentDictionary<Type, object> Stores { get; } =
new ConcurrentDictionary<Type, object>();
ValueStore<T> Store<T>() =>
(ValueStore<T>)Stores.GetOrAdd(typeof(T), t => new ValueStore<T>());
public T Add<T>(
ExtensionProperty<T> property,
T value) =>
Store<T>()
.Add(property, value);
public T Get<T>(
ExtensionProperty<T> property) =>
Store<T>()
.Get(property);
public T GetOrAdd<T>(
ExtensionProperty<T> property,
Func<T> valueFactory) =>
Store<T>()
.GetOrAdd(property, valueFactory);
}
and strictly typed ValueStore<T>
to bypass boxing/unboxing:
class ValueStore<T>
{
ConcurrentDictionary<ExtensionProperty<T>, T> Values { get; } =
new ConcurrentDictionary<ExtensionProperty<T>, T>();
public T Add(ExtensionProperty<T> property, T value)
{
if(!Values.TryAdd(property, value))
throw new RedefinedPropertyException(property);
return value;
}
public T Get(ExtensionProperty<T> property)
{
T value;
if(!Values.TryGetValue(property, out value))
throw new UndefinedPropertyException(property);
return value;
}
public T GetOrAdd(ExtensionProperty<T> property, Func<T> valueFactory) =>
Values.GetOrAdd(property, p => valueFactory());
}
Exceptions:
public abstract class ExtensionPropertyException : Exception
{
protected ExtensionPropertyException(string message)
: base(message)
{
}
}
public class UndefinedPropertyException : ExtensionPropertyException
{
public UndefinedPropertyException(IExtensionProperty property)
: base($"Undefined {property.Name}.")
{
}
}
public class RedefinedPropertyException : ExtensionPropertyException
{
public RedefinedPropertyException(IExtensionProperty property)
: base($"Redefined {property.Name}.")
{
}
}