Back to Blog
C#

How to Use C# Custom Attribute Classes

Learn how to define, apply, and read c# custom attribute classes, including AttributeUsage constraints and reflection-based retrieval.

C# attributesreflectionmetadatacode annotations
Illustration of a C# code block with a custom attribute label and reflection arrows showing runtime metadata lookup

When you apply an attribute to a class, method, or property in C#, the compiler stores that metadata in the assembly. A c# custom attribute is simply a class that inherits from System.Attribute, allowing you to attach your own metadata to code elements and then read it at runtime via reflection. This is useful for validation, serialization, routing, and other scenarios where behavior depends on declared intent rather than hardcoded logic.

Defining a Custom Attribute Class

The minimal definition of a custom attribute is a class that inherits from System.Attribute. By convention, attribute class names end with Attribute; the compiler lets you omit that suffix when applying the attribute.

public class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }

The constructor parameters become the positional arguments when you apply the attribute. For example:

[MaxLength(140)] public string Comment { get; set; }

When you apply [MaxLength(140)], the compiler creates an instance of MaxLengthAttribute and passes 140 to the constructor. That instance is serialized into the metadata, so the data is available later without re-executing the constructor logic.

Restricting Attribute Usage with AttributeUsage

By itself, a custom attribute can be applied to almost any code target: class, method, property, field, parameter, assembly, and more. Often you want to restrict where the attribute is legal. The AttributeUsage attribute controls that.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }

Now trying to apply [MaxLength] to a method produces a compile-time error: "Attribute 'MaxLength' is not valid on this declaration type." The valid targets are combined with the bitwise OR operator using the AttributeTargets enum.

AllowMultiple controls whether the same attribute can appear more than once on a single element. The default is false. If your attribute can meaningfully appear multiple times, set it to true and usually implement a property that returns the list of values.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class TagAttribute : Attribute { public string Label { get; } public TagAttribute(string label) { Label = label; } }

Inherited controls whether the attribute is inherited by classes that derive from a base class marked with it. The default is true for class attributes, but the behavior depends on how the attribute is queried. By default, reflection returns only the declared attributes unless you specifically ask for inherited ones.

Adding Properties to Expose Metadata

Attributes can expose data through constructor parameters (positional) or named properties (named arguments). Positional parameters are required at the call site; named properties are optional unless the property is set in the constructor.

[AttributeUsage(AttributeTargets.Property)] public class DisplayAttribute : Attribute { public string Name { get; } public int Order { get; set; } public DisplayAttribute(string name) { Name = name; } }

You can apply it with a positional argument and optionally a named argument:

[Display("User Name", Order = 2)] public string UserName { get; set; }

The named argument Order = 2 sets the property after the constructor runs. This is a common pattern for attributes that carry optional configuration data.

Reading Custom Attributes at Runtime

Attributes have no effect until something reads them. You normally use reflection to retrieve the attribute instances and inspect their properties.

The simplest way is to call GetCustomAttribute on a MemberInfo or ParameterInfo object.

using System.Reflection; PropertyInfo prop = typeof(Comment).GetProperty(nameof(Comment.Text)); MaxLengthAttribute attr = prop.GetCustomAttribute<MaxLengthAttribute>(); if (attr != null && value.Length > attr.Length) { throw new ArgumentException($"Text exceeds {attr.Length} characters."); }

For attributes that can appear multiple times, use GetCustomAttributes<T>() to get an array.

var tags = method.GetCustomAttributes<TagAttribute>(); foreach (var tag in tags) { Console.WriteLine(tag.Label); }

Reflection is the standard way to read attributes, but it has overhead. If you will query the same attribute often, cache the result in a dictionary keyed by the MemberInfo rather than calling GetCustomAttribute repeatedly.

Practical Example: Validation with Reflection

A common use of a c# custom attribute is declarative validation. Consider a validator that checks string properties annotated with a length limit.

Define the attribute:

[AttributeUsage(AttributeTargets.Property)] public class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }

Apply it to a model:

public class Comment { [MaxLength(140)] public string Text { get; set; } }

Write a generic validator that scans all public properties for the attribute and enforces the rule:

public static class Validator { public static void Validate(object obj) { var type = obj.GetType(); foreach (var prop in type.GetProperties()) { var attr = prop.GetCustomAttribute<MaxLengthAttribute>(); if (attr is null) continue; var value = prop.GetValue(obj) as string; if (value != null && value.Length > attr.Length) { throw new ValidationException( $"{prop.Name} exceeds maximum length of {attr.Length}."); } } } }

This keeps validation rules next to the data rather than scattered across controllers or services.

Performance and Caching Considerations

Reflection-based attribute reading involves metadata lookups and object allocation. If a validation path runs per request, retrieving the attribute every time can add overhead. In practice, GetCustomAttribute is not extremely expensive, but it performs a search over the metadata and creates a new instance each call.

To reduce that cost, cache the attribute data in a static dictionary:

private static readonly Dictionary<PropertyInfo, MaxLengthAttribute> Cache = new();
public static void Validate(object obj) { var type = obj.GetType(); foreach (var prop in type.GetProperties()) { if (!Cache.TryGetValue(prop, out var attr)) { attr = prop.GetCustomAttribute<MaxLengthAttribute>(); Cache[prop] = attr; } if (attr is null) continue; // ... validation logic } }

Because attribute instances are immutable after construction, caching them is safe. Be careful if you use async or multithreaded validation; use concurrent collections if the cache is shared across threads.

When Not to Use Custom Attributes

Custom attributes are not the right tool for every metadata problem. They are baked into the assembly at compile time, so you cannot change them at runtime without rebuilding. If you need dynamic metadata that can change based on configuration, a database, or user input, an attribute is the wrong fit.

Attributes also contribute to assembly size, though for most applications this is negligible. More importantly, reflection-based logic can be harder to debug because the execution path is indirect — you must trace how the attribute is interpreted.

Use a c# custom attribute when the metadata is static, tied to source code, and consumed by a generic mechanism such as validation, serialization, or routing. For dynamic rules, prefer configuration or a rule engine.

Compatibility and Versioning

Custom attributes are part of the ECMA C# standard and work across .NET Framework, .NET Core, and .NET 5+. When targeting multiple platforms, keep in mind that some attribute targets may not be supported on all runtime versions. For example, AttributeTargets.ReturnValue is technically supported but rarely used.

If you change the constructor signature of an attribute, any existing source that applies it will fail to compile. That is fine at compile time, but if you distribute a library as a binary, removing or changing a constructor parameter breaks consumers without recompilation. Keep attribute constructors stable or provide obsolete overloads.

Because attributes are part of the assembly metadata, they are part of your public API. Treat their design as a contract: once you ship a library with [MaxLength], changing its meaning can silently break consumers.

c# custom attribute: Practical Usage and Code Examples | RYUSLOG DEV