Collin Barrett
Collin Barrett

Reputation: 2569

Get attribute on or name of derived class's property

We have a BaseClass and a set of derived POCO classes (see DerivedClassA). They map to an existing key-value store in the database that we cannot change at this point.

Each property on the derived class will map to a key in the store. The keys that the properties represent are string values that are either the property name or (if present) the custom attribute MyAttribute.Key as demonstrated on PropertyC below. The common use case for the attribute is if the key begins with an integer which is invalid for a C# property name (and we cannot change the keys).

public class BaseClass
{
    public int BaseClass Id { get; set; } = 0;
}

public class DerivedClassA : BaseClass
{
    public int PropertyA { get; set; }
    public int PropertyB { get; set; }

    [MyAttribute(Key = "404Property")]
    public int PropertyC { get; set; }
}

In code, we need to get the key values as strings. After some wrestling and digging from other SO Answers (I do not claim any level of expertise with generics), we came up with the GetKey() method in the derived BaseClass<T> below. Note that GetCustomAttributeValue<T>() is a custom helper method that returns the value of an attribute (there may be a better way to do that, but that's out of scope for this question).

public class BaseClass<T> : BaseClass where T : BaseClass<T>
{
    public string GetKey(Expression<Func<T, object>> property)
    {
        var memberInfo = GetMemberInfo(property);
        return GetAttributeKey(memberInfo) ?? memberInfo?.Name;
    }

    private static MemberInfo GetMemberInfo(Expression<Func<T, object>> property) =>
        (property.Body as MemberExpression ?? ((UnaryExpression)property.Body).Operand as MemberExpression)?.Member;

    private static string GetAttributeKey(MemberInfo member) =>
        member.GetCustomAttributeValue<string>(typeof(MyAttribute), "Key");
}

This solution seems to work if we derive the classes from the new BaseClass<T>

    public class DerivedClassA : BaseClass<T> {
        ...
    }

The GetKey() is now called as follows:

var derivedClassA = new DerivedClassA();
var propertyCKey = derivedClassA.GetKey(p => p.PropertyC);

We have a requirement that BaseClass needs to stay around, however, and we do not want the complexity of both the non-generic and the generic versions of BaseClass.

When I try to move GetKey() into the non-generic BaseClass, it no longer has the T type of the derived class which it needs to lookup the set of properties on the derived class. I do not want to add duplicate GetKey()s in each derived class.

Question:

Is there a way to move the GetKey() method (possibly re-writing it) into BaseClass rather than introducing the new BaseClass<T> only for supporting GetKey()?

Additional Background:

We are trying to wrap object-oriented/strong typing around a data store that is just a table that looks like:

| PreferenceId | UserId | PreferenceString |

Each derived class represents a different PreferenceId. Each PreferenceString is just a string of key/values "serialized" in a way custom to that PreferenceId (there is no rhyme/reason that can be shared across all PreferenceIds). This should all be redesigned at some point, but we are trying to wrap the current store in some kind of strong typing as a step in the transition.

Upvotes: 4

Views: 892

Answers (1)

Yeldar Kurmangaliyev
Yeldar Kurmangaliyev

Reputation: 34244

As for me, all this structure in general seems to be crazy and overly complex.
Consider rewriting the whole approach instead of changing the GetKey method.

In general, GetKey in your base class breaks single-responsibility principle.

If I had to do this, I would just extract this functionality into a static class:

public static class CustomKeysHelper
{
    public static string GetKey<T>(Expression<Func<T, object>> property) where T : BaseClass
    {
        var memberInfo = GetMemberInfo(property);
        return GetAttributeKey(memberInfo) ?? memberInfo?.Name;
    }

    private static MemberInfo GetMemberInfo<T>(Expression<Func<T, object>> property) =>
        (property.Body as MemberExpression ?? ((UnaryExpression)property.Body).Operand as MemberExpression)?.Member;

    private static string GetAttributeKey<T>(MemberInfo member) =>
        member.GetCustomAttributeValue<string>(typeof(MyAttribute), "Key");
}

// Usage:
string cKey = CustomKeysHelper.GetKey<DerivedClassA>(dca => dca.PropertyC);

Yes, it makes every GetKey call look longer, but it separates this logic and makes your intention clear.

Just in case you have instance of object BaseClass and want to extract property name from instance, but not type, then you can an extension method:

public static class CustomKeysHelper
{
    // ... see above

    public static string GetKey<T>(this T obj, Expression<Func<T, object>> property) where T : BaseClass
    {
        return GetKey<T>(property);
    }
}    

// Now, you can do this:
DerivedClassA derAInstance = ...;
derAInstance.GetKey(dca => dca.PropertyC);

Upvotes: 3

Related Questions