6
\$\begingroup\$

I have a use-case where I need to retrieve all members with specific attributes in the class and interface hierarchy - I usually need the first match and apply its rules to child members. The built-in GetCustomAttributes are too limited becuase they work only for a single member and don't support interfaces.


Implementation

To solve this I wrote my own extension that returns a collection of AttributeCollection<T> instances. Each one contains the member the attributes are applied to and the matched attributes.

There are couple of rules that this needs to follow in order for the results to be useful because attribute settings are then propagated to child members:

  • properties come before types
  • classes come before interfaces
  • skip duplicate results
public static class Extensions
{
    public static IEnumerable<AttributeCollection<T>> EnumerateCustomAttributes<T>(this MemberInfo member) where T : Attribute
    {
        if (member == null) throw new ArgumentNullException(nameof(member));

        var queue = new Queue<MemberInfo>
        {
            member,
        };

        // Helps to suppress duplicate results when same member is seen multiple times.
        var seenAttributeCollections = new HashSet<AttributeCollection<T>>();

        while (queue.Any())
        {
            var current = queue.Dequeue();

            if (current.GetCustomAttributes<T>() is var attributes && attributes.Any())
            {
                var attributeCollection = new AttributeCollection<T>(current, attributes);
                if (seenAttributeCollections.Add(attributeCollection))
                {
                    yield return attributeCollection;
                }
            }

            if (current is PropertyInfo property)
            {
                queue.Enqueue(property.DeclaringType);
            }

            if (current is Type type)
            {
                // The order matters so enqueue properties before their declaring types and base classes before interfaces.

                if (type.IsSubclass())
                {
                    if (type.BaseType.GetProperty(member.Name) is PropertyInfo otherProperty)
                    {
                        queue.Enqueue(otherProperty);
                    }

                    queue.Enqueue(type.BaseType);
                }

                foreach (var interfaceType in type.GetInterfaces())
                {
                    if (interfaceType.GetProperty(member.Name) is PropertyInfo otherProperty)
                    {
                        queue.Enqueue(otherProperty);
                    }

                    queue.Enqueue(interfaceType);
                }
            }
        }
    }

    public static bool IsSubclass(this Type type)
    {
        return type.IsClass && type.BaseType != typeof(object);
    }   
}

This class helps handling equality and results:

public class AttributeCollection<T> : List<T>, IEquatable<AttributeCollection<T>> where T : Attribute
{
    private static readonly IEqualityComparer<AttributeCollection<T>> Comparer = EqualityComparerFactory<AttributeCollection<T>>.Create
    (
        // When either one is True then we consider both collections equal.
        equals: (x, y) => (x.Member == y.Member) || x.SequenceEqual(y)
    );

    public AttributeCollection(MemberInfo member, IEnumerable<T> attributes) : base(attributes)
    {
        Member = member;
    }

    public MemberInfo Member { get; }

    public bool Equals(AttributeCollection<T> other) => Comparer.Equals(this, other);

    public override bool Equals(object obj) => obj is AttributeCollection<T> ac && Equals(ac);

    public override int GetHashCode() => 0; // Always use 'equals'.

    public override string ToString()
    {
        return $"{Member.Name}: [{string.Join(", ", this.Select(a => a))}]";
    }
}

Demo

I used this code to test that extension:

void Main()
{
    typeof(T3).GetProperty(nameof(T3.P1)).EnumerateCustomAttributes<A0>().Select(x => x.ToString()).Dump(); // <-- 6 results
    typeof(T3).GetProperty(nameof(T3.P1)).EnumerateCustomAttributes<A1>().Select(x => x.ToString()).Dump(); // <-- 5 results
    typeof(T3).GetProperty(nameof(T3.P1)).EnumerateCustomAttributes<A2>().Select(x => x.ToString()).Dump(); // <-- 3 results
}

[A1(V = "I1")]
interface I1
{
    [A1(V = "I1.P1")]
    string P1 { get; set; }
}

[A2(V = "T1")]
class T1 : I1
{
    [A1(V = "T1.P1")]
    public virtual string P1 { get; set; }
}

class T2 : T1 { }

[A1(V = "T3"), A2(V = "T3")]
class T3 : T2
{
    [A1(V = "T3.P1"), A2(V = "T3.P1")]
    public override string P1 { get; set; }
}

interface IA
{
    string V { get; set; }
}
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
abstract class A0 : Attribute, IA { public abstract string V { get; set; } public override string ToString() => V; }
class A1 : A0 { public override string V { get; set; } }
class A2 : A0 { public override string V { get; set; } }

Results:

IEnumerable<String> (6 items)

P1: [T3.P1, T3.P1] 
T3: [T3, T3] 
P1: [T1.P1] 
T2: [T1] 
P1: [I1.P1] 
I1: [I1] 


IEnumerable<String> (5 items)

P1: [T3.P1] 
T3: [T3] 
P1: [T1.P1] 
P1: [I1.P1] 
I1: [I1] 


IEnumerable<String> (3 items)

P1: [T3.P1] 
T3: [T3] 
T2: [T1]

In this example you'll notice that I use both an interface and an abstract class overriding the V property. It turned out that I cannot use a single property on the base class because the Attribute.Equals method won't see it and will not recognize two different attributes correctly. See this question.


If you're going to try this demo in LINQPad then you'll need this header as I'm using some of my helpers here:

<Query Kind="Program">
  <NuGetReference>Reusable.Core</NuGetReference>
  <Namespace>Reusable.Extensions</Namespace>
  <Namespace>Reusable.Collections</Namespace>
</Query>

Real-world example

I'll be using it for retrieving UseX attributes in a model like this one:

[UsePrefix("app"), UseNamespace, UseType, UseMember]
[TrimStart("I")]
public interface IDemo : INamespace
{
    [UseType, UseMember]
    object Greeting { get; } // <-- will use its own attributes

    [Tag("io")]
    object ReadFile { get; } // <-- will use type's attributes
}

Questions

So, what do you think about this implementation? Am I missing anything important here? Is there anything you would improve?

\$\endgroup\$
  • \$\begingroup\$ It is a bit unclear comparing your test results with your specification what the exact order of results should be. It seems the specified order is subordinate to the class hierarchy order. Could you elaborate on this? \$\endgroup\$ – dfhwze Jun 13 at 9:00
  • 1
    \$\begingroup\$ @dfhwze exactly! The class hierarchy matters because later attribute-sets can override previous ones. When a base class specifies some properties then all subsequent members inherit them... unless they redefine their own set of attributes. They are not merged, this would be too tricky. I think it's easier to just completely specify a new set of attributes than trying to disable some etc. \$\endgroup\$ – t3chb0t Jun 13 at 9:02
5
\$\begingroup\$
    public static IEnumerable<AttributeCollection<T>> EnumerateCustomAttributes<T>(this MemberInfo member) where T : Attribute

I'm surprised that this method doesn't have a docstring, and I don't find its name very descriptive. What differentiates this from GetCustomAttributes is that it inherits, so I'd expect a name like InheritedCustomAttributes. (Perhaps there's an equally succinct name which makes it clear that it includes attributes defined directly on the member).


            var queue = new Queue<MemberInfo>
            {
                member,
            };

Is a queue the right data structure? Bearing in mind that you say

  • properties come before types
  • classes come before interfaces

I would have thought that you need to fill a priority queue in one pass (properties have priority over classes have priority over interfaces; and for properties and classes the nearer one has priority; priority between interfaces seems rather arbitrary) and then call GetCustomAttributes in a second pass.


            // Helps to suppress duplicate results when same member is seen multiple times.
            var seenAttributeCollections = new HashSet<AttributeCollection<T>>();

I'm not clear on why duplication is handled at the level of collections of attributes rather than individual attributes.


                    if (type.IsSubclass())
                    {
                        if (type.BaseType.GetProperty(member.Name) is PropertyInfo otherProperty)
                        {
                            queue.Enqueue(otherProperty);
                        }

                        queue.Enqueue(type.BaseType);
                    }

                    foreach (var interfaceType in type.GetInterfaces())
                    {
                        if (interfaceType.GetProperty(member.Name) is PropertyInfo otherProperty)
                        {
                            queue.Enqueue(otherProperty);
                        }

                        queue.Enqueue(interfaceType);
                    }

Firstly, I think it would be cleaner to extract a method GetSupertypes which returns the base class (if there is one) followed by the interfaces, so that the loops can be combined into one.

Secondly, I think there are a couple of problems with the PropertyInfo handling:

  1. member is not necessarily a PropertyInfo. Should the signature of the method be changed? If not, should support be added for MethodInfo too?
  2. Consider

    class T4 : T3
    {
        public new string P1 { get; set; }
    }
    

    Should T4.P1 inherit attributes from T3.P1?


public class AttributeCollection<T> : List<T>, IEquatable<AttributeCollection<T>> where T : Attribute
{
    private static readonly IEqualityComparer<AttributeCollection<T>> Comparer = EqualityComparerFactory<AttributeCollection<T>>.Create
    (
        // When either one is True then we consider both collections equal.
        equals: (x, y) => (x.Member == y.Member) || x.SequenceEqual(y)
    );

To my comment above that I'm not clear on why duplication is handled at the level of collections of attributes rather than individual attributes, I really don't understand why the order would matter. If collections of attributes are the correct level, should they not at least be treated as sets and equality with SetEquals?

\$\endgroup\$
  • \$\begingroup\$ I treat each set of attributes as a whole so that later attribute-sets can override previous ones. If I treated them individually then I would merge them or would need another mechanism of disabling previous definitions. \$\endgroup\$ – t3chb0t Jun 13 at 9:01
  • 1
    \$\begingroup\$ ok, I think this is a dead-end solution. I'll follow only base classes and interfaces not. There is no way to come up with a reasonable set of rules for them as their order can change anytime and could break everything. \$\endgroup\$ – t3chb0t Jun 13 at 11:32

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.