Reflection lets you associate metadata with types and query it at runtime through the IL/MSIL metadata stored in the portable executable (PE) file. Custom attributes provide a clean way to attach such metadata — here, to tag filter classes with their data type category so the category is declared in one place and read everywhere else.
Example 1 — Without Attributes
Each filter class defines its category as an abstract property override, and the strategy dictionary re-states it as a key. Two definitions of the same fact:
// Without attributes — the data type category is hard-coded in each class
public abstract class FilterClassic
{
public abstract FilterDataType FilterDataType { get; }
public abstract bool Test(object a);
}
public class FilterClassicIsInteger : FilterClassic
{
// Category defined here...
public override FilterDataType FilterDataType => FilterDataType.Integer;
public override bool Test(object a)
{
try { Convert.ToInt32(a); return true; }
catch { return false; }
}
}
public class FilterClassicIsText : FilterClassic
{
public override FilterDataType FilterDataType => FilterDataType.Text;
public override bool Test(object a)
{
try { Convert.ToString(a); return true; }
catch { return false; }
}
}// ...and the same category must be repeated in the dictionary — two definitions, one truth.
static Dictionary<FilterDataType, Func<FilterClassic>> filters =
new Dictionary<FilterDataType, Func<FilterClassic>>
{
{ FilterDataType.Integer, () => new FilterClassicIsInteger() },
{ FilterDataType.Text, () => new FilterClassicIsText() },
};
// Adding a new filter type requires updating both the class AND this dictionary.Example 2 — With Attributes
Defining a custom attribute is as simple as inheriting from System.Attribute. A small helper class reads it back via GetCustomAttribute<T>():
// Define the attribute — inherit from System.Attribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class FilterDataTypeAttribute : Attribute
{
public FilterDataType FilterDataType { get; }
public FilterDataTypeAttribute(FilterDataType filterDataType)
{
FilterDataType = filterDataType;
}
}
// Helper that reads the attribute from a given type via reflection
public class FilterMetadata
{
public FilterDataType FilterDataType { get; private set; }
public FilterMetadata(Type t)
{
FilterDataType = t.GetCustomAttribute<FilterDataTypeAttribute>().FilterDataType;
}
}The base class reads its own attribute in the constructor. Concrete subclasses only declare the category once — as a decoration:
// The base class reads its own type's attribute in the constructor
public abstract class Filter
{
public FilterMetadata FilterMetadata { get; private set; }
public abstract bool Test(object a);
protected Filter()
{
// Read the [FilterDataType(...)] attribute from the concrete subclass
FilterMetadata = new FilterMetadata(this.GetType());
}
}
// Category is now declared once — on the class, not duplicated in a dictionary
[FilterDataType(FilterDataType.Text)]
public class FilterIsText : Filter
{
public override bool Test(object a)
{
try { Convert.ToString(a); return true; }
catch { return false; }
}
}
[FilterDataType(FilterDataType.Integer)]
public class FilterIsInteger : Filter
{
public override bool Test(object a)
{
try { Convert.ToInt32(a); return true; }
catch { return false; }
}
}Usage with Attributes
The registration helper infers the key from the type parameter so the dictionary is populated without repeating the category:
// Dictionary is built by reading the attribute — no duplicate category definition
static Dictionary<FilterDataType, Func<Filter>> filtersWithMeta =
new Dictionary<FilterDataType, Func<Filter>>();
// Helper reads the FilterDataType attribute from the type parameter T
static void AddFilter<T>(Func<T> generator) where T : Filter
{
var key = new FilterMetadata(typeof(T)).FilterDataType;
filtersWithMeta.Add(key, generator);
}
// Visual Studio infers T from the lambda — explicit type parameter is optional
AddFilter(() => new FilterIsText()); // key = FilterDataType.Text
AddFilter(() => new FilterIsInteger()); // key = FilterDataType.Integer
// Adding a new filter type: create the class with [FilterDataType(...)], call AddFilter — done.