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?