6
\$\begingroup\$

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 property
  • Salutation - 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}.")
    {
    }
}
\$\endgroup\$
3
\$\begingroup\$
  1. API in your example is really confusing, since you use Name method to both set and get property value. You should make your intent clear, by using, say, GetName and SetName for method names.
  2. You should probably add T Value property to your ExtensionProperty<T> class, and use it to store the value, instead of ConcurrentDictionary. I think it will make more sense.
  3. IExtensionProperty interface is awkward. I think you can do just fine without it.
  4. Finally, imho, you are not making a convincing case. Why can't you just create a regular class (or classes) to analyze data and another class (or classes) to store the results? Call me old-fashioned, but the idea of using attached properties to analyze data strikes me as a truly horrifying design decision. At least it is not something I am willing to maintain, debug or review. :)
\$\endgroup\$
  • \$\begingroup\$ 1) It is a jQuery influence where we mimic properties by functions. What about Name()/SetName(value) pair? 2) It is a WPF influence. We can not have Value property on ExtentionProperty<T>: there are 0..n values per 1 property. 3) IExtensionProperty is used to generate exceptions. 4) It is like WPF attached properties. The same situation, the same solution. \$\endgroup\$ – Dmitry Nogin Jan 26 '16 at 14:37
  • \$\begingroup\$ 1) I would still prefer my version. It is a common naming pattern in .Net and most developers will be used to it. 2) I don't follow. 3) No, you use IExtensionProperty.Name property, which you can pass to exception constructor directly. 4) Your situation is nothing like we have in WPF. DependencyProperty is there to support data binding, PropertyChanged notifications, styles, default values, etc. That is why its obscure syntax and implementation are justified. The situation you described in your question on the other hand is fairly straightforward and can be solved using regular patterns. \$\endgroup\$ – Nikita B Jan 26 '16 at 15:02
  • \$\begingroup\$ 1) I prefer to do not use Get prefix in domain logic. There is a good explanation in Vernon's book. 2) How many values we have per instances of WPF DependencyProperty? It is many to one. 3) DRY - I will need to resolve to the name every time constructing exception. Primitive Obsession - PropertyException logically intended to express property problem, not name problem. 4) It is not like DependancyProperty, it is exactly the same thing as AttachedProperty. There are two big differences :) \$\endgroup\$ – Dmitry Nogin Jan 26 '16 at 15:21
  • \$\begingroup\$ 4) The solution is similar, I agree. I argue your point about the problem being the same. Problem of analyzing data and storing results is nothing like building a UI framework. It is a straightforward problem, which you are trying really hard to make complex by using the wrong tool. I had a lot of "fun" debugging interactions between DependencyPropertiesin the past, and in my opinion it is not something other people should look forward to. But oh well, I mean its your code, do what you feel is right. :) \$\endgroup\$ – Nikita B Jan 27 '16 at 7:10
  • \$\begingroup\$ 3) There is also KISS and YAGNI. But those are clearly not your favorites. :) \$\endgroup\$ – Nikita B Jan 27 '16 at 7:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.