2

I have a class that is declared Internal. It is decorated with various annotations. In particular is the [DisplayName("My Display Name")] annotation. I have some code that will retrieve the value but only works if the class is declared public. I am sort of new to using reflection. I believe I need to specify that the BindingFlags.NonPublic be used but I am not sure where.

LinqPAD code:

void Main()
{
    List<SpGetProfileInfoResult> p = new List<SpGetProfileInfoResult>();
    p.Add(new SpGetProfileInfoResult() { FName = "Eric" });
    p.Add(new SpGetProfileInfoResult() { FName = "Mike" });

    p.Dump();

    foreach (var item in p)
    {
        Console.WriteLine(item.DisplayName(i => i.FName));
        Console.WriteLine(item.FName);
    }

}

public partial class SpGetProfileInfoResult
{
    // Uncomment this annotation to see that this part will work
    // [System.ComponentModel.DisplayNameAttribute("[BILLTO-FNAME]")]
    public string FName { get; set; }
}

public partial class SpGetProfileInfoResult
{
    internal class Metadata
    {
        // This attribute is never available seems.
        [System.ComponentModel.DisplayNameAttribute("[BILL-FNAME]")]
        public string FName { get; set; }
    }
}

public static class Tag
{
    public static T GetAttribute<T>(this MemberInfo member, bool isRequired) where T : Attribute
    {
        var attribute = member.GetCustomAttributes(typeof(T), false).SingleOrDefault();

        if (attribute == null && isRequired)
        {
            throw new ArgumentException(
                string.Format(
                "The {0} attribute must be defined on member {1}",
                typeof(T).Name,
                member.Name));
        }

        return (T)attribute;
    }

    public static string DisplayName<T>(this T src,Expression<Func<T, object>> propertyExpression)
    {
        Type metadata = null;

        var memberInfo = GetPropertyInformation(propertyExpression.Body);
        if (memberInfo == null)
        {
            throw new ArgumentException(
                "No property reference expression was found.",
                "propertyExpression");
        }

        var attr = memberInfo.GetAttribute<DisplayNameAttribute>(false);
        if (attr == null)
        {
            return memberInfo.Name;
        }

        return attr.DisplayName;
    }

    public static MemberInfo GetPropertyInformation(Expression propertyExpression)
    {
        MemberExpression memberExpr = propertyExpression as MemberExpression;
        if (memberExpr == null)
        {
            UnaryExpression unaryExpr = propertyExpression as UnaryExpression;
            if (unaryExpr != null && unaryExpr.NodeType == ExpressionType.Convert)
            {
                memberExpr = unaryExpr.Operand as MemberExpression;
            }
        }

        if (memberExpr != null && memberExpr.Member.MemberType == MemberTypes.Property)
        {
            return memberExpr.Member;
        }

        return null;
    }
}

Usage:

If you don't have LinqPAD, you should download it then you can test this pretty easily by just creating a new C# Program in LinkPAD

Debug.WriteLine(item.DisplayName(i => i.FName));
ewahner
  • 1,149
  • 2
  • 11
  • 23
  • Works when I try it. What goes wrong when you try it? Also, it's good that you have included code, but a minimal *compiling* example would really help. – AakashM Jan 09 '12 at 13:56
  • There are two partial classes, one that has many attributes and that is the generated class and then the one above, which is the metadata class. I can only get to the attributes of the generated class and none of the internal class. The project is sort of huge so I was unable to create such a small example. I tried to bake it down to the most basic components. – ewahner Jan 09 '12 at 14:58
  • Yeah, it's tricky I know. But given that there's a difference in behaviour between `internal` and `public`, we really need to see where the *assembly* dividing lines are. As things stand, the only *reflection* call you're making is the `GetCustomAttributes` one, and that doesn't have any `BindingFlags` parameters. – AakashM Jan 09 '12 at 15:31
  • Updated with new example...this one is pretty concise and you can uncomment the working property to see the difference. – ewahner Jan 09 '12 at 15:36
  • Now I'm not sure what you're *expecting* to happen. Your `GetAttribute` method is looking for an attribute on *the member it is passed*, which here is the `FName` property on a `SpGetProfileInfoResult`. You're not referring to `SpGetProfileInfoResult.Metadata` (which. although nested. is a **different** class) anywhere, so I'm not sure when you expect it to be looked at. The `metadata` variable in `DisplayName` is not used in the current code - is there more to see? – AakashM Jan 09 '12 at 15:53
  • What would I need to change in order to make this work? – ewahner Jan 09 '12 at 16:11
  • It seems like you're confusing the visibility of a class with whether or not it's nested. You are never using your nested MetaData class in this example. To use it from outside of SpGetPrifileInforResult you would have to refer to as SpGetProfileInfoResult.Metadata. As long as you're in the same assembly, that will work. – Igby Largeman Jan 09 '12 at 17:05
  • I completely understand the visibility of the nested class. I just have no control over how its generated because I am using a third-party tool CodeSmithTools. My question is how do I target that Internal Metadata class? – ewahner Jan 09 '12 at 18:26

3 Answers3

3

So it looks like you want to be able to decorate existing members of a partial class, by providing metadata in a separate partial piece. There's no built-in mechanism for that (see eg this question and the classes mentioned in the answer), but if you're willing to stick to a convention, you can roll your own:

So suppose we have

public partial class SpGetProfileInfoResult
{
    public string FName { get; set; }
}

in a partial piece we can't change, and

public partial class SpGetProfileInfoResult
{
    internal class Metadata
    {
        [System.ComponentModel.DisplayNameAttribute("[BILL-FNAME]")]
        public string FName { get; set; }
    }
}

in a partial piece we can change. You already have most of the pieces: in DisplayName(), you successfully determine that we are looking at the FName property; you then look for a DisplayNameAttribute on T.FName, but there isn't one, so that's where it stops.

What you need to do is, in the case where you don't find the attribute you need,

var attr = memberInfo.GetAttribute<DisplayNameAttribute>(false);
if (attr == null)
{

Look for a nested class named Metadata - note here is one place we use BindingFlags.NonPublic

    // Try and get a nested metadata class
    var metadataType = typeof(T)
        .GetNestedType("Metadata", 
                       BindingFlags.Public | BindingFlags.NonPublic);

If we find one:

    if (metadataType != null)
    {

Look for a member of the same name as was originally being talked about (BindingFlags.NonPublic again)

        var membersOnMetadataType = metadataType.GetMember(memberInfo.Name, 
            BindingFlags.Instance |
            BindingFlags.Public | 
            BindingFlags.NonPublic);

If there is one, use your helper method, but this time pass it the metadata type's member:

        if (membersOnMetadataType.Any())
        {
            var attrOnMetadataType = membersOnMetadataType[0]
                .GetAttribute<DisplayNameAttribute>(false);
            return attrOnMetadataType.DisplayName;

(I've omitted a final nullity check here, as well as closing the control flow)

Depending on how distasteful you find that "Metadata" string, you could instead do something declarative with attributes:

  • have a class-level attribute that goes on SpGetProfileInfoResult (the piece you can change) that points at its Metadata using typeof (this is the approach taken by System.ComponentModel), or
  • have a class-level attribute that goes on Metadata, to have it claim 'I am a metadata type'. Then instead of searching for a nested class named a fixed string, we would instead search for a nested class having this particular attribute.
Community
  • 1
  • 1
AakashM
  • 62,551
  • 17
  • 151
  • 186
  • Well thought out answer. Probably a little more elegant than my solution but it looks like we sort of came up with the same answer. – ewahner Jan 10 '12 at 13:08
1

After working on this for a while I came up with a Hack. I am sure someone out there can help me clean this up a bit, but this is what I found works. I had to add the "Metadata" nested class to the DeclaringType and then do a GetMember on that result, which returns a collection of members.

public static string DisplayName<T>(this T src, Expression<Func<T, object>> propertyExpression)
{
    var memberInfo = GetPropertyInformation(propertyExpression.Body);
    var mytype = src.GetType();
    string strType = mytype.Name + "+Metadata";
    var metaType = Type.GetType(strType);
    MemberInfo[] mem = metaType.GetMember(memberInfo.Name);
    var att = mem[0].GetCustomAttributes(typeof(DisplayNameAttribute), true).FirstOrDefault() as DisplayNameAttribute;

    if (att == null)
        return memberInfo.Name;
    else
        return att.DisplayName;
}
ewahner
  • 1,149
  • 2
  • 11
  • 23
0

I won't try to debug your code because you're using some classes that I'm not familiar with.

One thing I do know is that MemberInfo does not have a GetAttribute() function. You must be using an extension method there.

However I can tell you that you don't need any special bindingflags just because the type is internal. Only the visibility of the member is important, and in this case it's public.

using System;
using System.ComponentModel;

namespace ConsoleApplication1
{
    internal class Metadata
    {
        [DisplayName("[BILL-FNAME]")]
        public string FName { get; set; }
    } 

    class Program
    {
        static void Main()
        {
            var memberInfo = typeof(Metadata).GetMember("FName")[0];
            var atrributes = memberInfo.GetCustomAttributes(false);
            Console.WriteLine(atrributes[0].GetType().Name);
        }
    }
}

Output:

DisplayNameAttribute

Igby Largeman
  • 16,495
  • 3
  • 60
  • 86
  • I updated my example. You were right about the GetAttribute. I added the function just so the whole example is clear. I also updated the Class to show how the tool is generating the class. Since Metadata is internal, I cannot access it in the manner you have shown. – ewahner Jan 09 '12 at 12:02