C# Attribute Declaration Syntax and Usage
c# attribute declaration: Learn the exact syntax for declaring custom attributes in C#, including constructors, properties, targets, and retrieval via reflection.
Declaring a custom attribute in C# starts with a class that inherits from System.Attribute and is marked with the [AttributeUsage] attribute. The core of c# attribute declaration is straightforward: you create a class, decorate it with AttributeUsage, define constructors and properties, and then apply it to code elements. However, the details of constructor parameters, property initialization, and how reflection reads the attribute determine whether your attribute works reliably in production.
The Minimal Declarations
A basic attribute declaration looks like this:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class AuthorAttribute : Attribute { public string Name { get; } public AuthorAttribute(string name) { Name = name; } }
This declares an attribute named Author. The [AttributeUsage] line limits where the attribute can be applied—in this case, to classes and methods. The sealed modifier is common but optional; sealing prevents inheritance from the attribute class, which usually is not needed.
The class name ends with Attribute by convention. When you apply it in code, you can drop the suffix. Both [Author("Ada")] and [AuthorAttribute("Ada")] are valid, but the shorter form is idiomatic.
Constructor Parameters and Named Properties
You can define a constructor that accepts parameters. These become the positional arguments used when applying the attribute. In addition, you can define public read-write properties that can be set using named arguments in the attribute application.
[AttributeUsage(AttributeTargets.Class)] public sealed class VersionAttribute : Attribute { public int Major { get; } public int Minor { get; } public string? Status { get; set; } public VersionAttribute(int major, int minor) { Major = major; Minor = minor; } }
Usage:
[Version(2, 1, Status = "Preview")] public class ApiClient { }
The constructor parameters major and minor are compulsory positional arguments. Status is an optional named property. Named properties must be public and have both getter and setter. Read-only properties cannot be set via named arguments because the attribute application syntax requires a settable property.
When you apply an attribute, the positional arguments must match the constructor signature. The compiler validates this at compile time, so a mismatch results in a compile error. Named arguments are evaluated in no particular order, and each property can be assigned only once.
Attribute Targets
AttributeUsage specifies where the attribute can be placed. The AttributeTargets enum includes Class, Method, Property, Field, Parameter, ReturnValue, Assembly, and others. You can combine multiple targets with the bitwise OR operator:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] public sealed class LogAttribute : Attribute { }
If you omit AttributeUsage, the attribute can be applied to any target. That permissiveness can hide mistakes, so it is usually better to specify allowed targets explicitly.
For assembly-level attributes, you apply the attribute outside any namespace, directly in the file:
[assembly: AssemblyTitle("MyApp")]
The target specifier assembly: is required for attributes that target the assembly, such as AssemblyTitle or custom assembly metadata.
Retrieving Attributes via Reflection
Declaring an attribute is only half the work. The attribute becomes useful when code queries it at runtime using reflection. The typical pattern is to call GetCustomAttribute or GetCustomAttributes on the MemberInfo of the decorated element.
Type type = typeof(ApiClient); var version = type.GetCustomAttribute<VersionAttribute>(); if (version is not null) { Console.WriteLine($"Version {version.Major}.{version.Minor} {version.Status}"); }
GetCustomAttribute<T> returns the first attribute of the requested type, or null if none is present. If the attribute can appear multiple times, use GetCustomAttributes<T>().
This querying is the reason custom attributes are not just decorative. They provide metadata that other parts of the system can act on—for example, a validation framework checking [Required] attributes on properties, or a test runner reading [TestMethod].
Attribute Usage and Compatibility
A few restrictions apply to attribute declarations. The attribute class must be a direct or indirect subclass of System.Attribute. C# does not allow generic attribute classes—Attribute<T> is not permitted. Attributes cannot use ref or out parameters, and they cannot be written to after construction except through settable properties. Also, attribute constructor arguments are limited to certain types: primitive types, string, Type, enum, and one-dimensional arrays of those types. Passing a List<string> or a delegate will cause a compile error.
[AttributeUsage(AttributeTargets.Class)] public sealed class DataAttribute : Attribute { public DataAttribute(Type dataType) { } } [Data(typeof(MyDto))] public class Repository { }
Here the Type argument is allowed because typeof returns a Type instance. Passing a complex object constructed inline is impossible. This restriction exists because attribute data is stored in metadata, which is limited to simple, self-contained values.
Another limitation is that the attribute constructor is called when the attribute is instantiated via reflection. If the constructor throws, the exception propagates to the code that calls GetCustomAttribute. Keep constructors free of side effects and avoid resource-heavy initialization.
Inherited Attributes and Overriding
By default, if you apply an attribute to a base class, derived classes also see that attribute when you query with GetCustomAttribute on the derived type. This behavior is controlled by the Inherited property of AttributeUsage.
[AttributeUsage(AttributeTargets.Class, Inherited = true)] public sealed class MarkerAttribute : Attribute { } [Marker] public class Base { } public class Derived : Base { }
Querying typeof(Derived).GetCustomAttribute<MarkerAttribute>() returns the attribute from Base. If you set Inherited = false, the derived class does not see it. This behavior also applies to interface implementations: attributes on the interface are not inherited by the implementing class by default.
Note that GetCustomAttribute on a method uses the same inheritance rules, but for methods overridden in derived classes, the behavior is slightly different. The base method's attributes are inherited only if the attribute's Inherited property is true and the override does not specify its own attribute. If the override has its own attribute, that one wins.
Performance Considerations
Attribute retrieval via reflection is not free. Each call to GetCustomAttribute involves metadata lookup and possibly instantiating the attribute object. In hot paths—such as a request handler that queries attributes on every request—this overhead can matter. The solution is usually to cache the result.
private static readonly ConcurrentDictionary<Type, VersionAttribute?> Cache = new(); public static VersionAttribute? GetCachedVersion(Type type) { return Cache.GetOrAdd(type, t => t.GetCustomAttribute<VersionAttribute>()); }
Caching the attribute instance avoids repeated reflection calls. If the attribute is stateless, you can even cache a singleton. But if the attribute has properties that are set via named arguments, they are set once at construction time; caching the same instance is safe because attribute objects are immutable by convention.
Do not use attributes as a substitute for runtime logic that depends on dynamic state. Attributes are static metadata defined at compile time. They are not suitable for values that change during execution, such as per-user permissions or things that derive from configuration. For those, use a configuration system or a database lookup.
Maintainability and Code Clarity
When you declare many custom attributes, the codebase can become cluttered with small classes. Keep the attribute class and its usage close together unless the attribute is shared across projects. If an attribute is only used in one assembly, it is often fine to place it in the same file as the main consumer, or in a Attributes folder if you prefer grouping.
Name the attribute class with the standard suffix and provide XML documentation to explain the intended meaning. Since attributes are exposed to reflection, other developers can rely on the attribute's contract; an undocumented property like Status can be misinterpreted.
A less obvious maintainability issue is that changing the attribute's constructor signature breaks all usages at compile time. Unlike a runtime configuration file, attribute applications are source code. Rename parameters carefully; a rename can cause a breaking change if the parameter was used as a named argument. Use the Obsolete attribute to mark old variants if necessary.
The decision to use attributes over a simpler approach comes down to whether the metadata needs to be discovered at runtime. If you simply want to mark a class for your own reading, consider a comment or a naming convention. Attributes shine when framework code needs to discover the metadata without a custom configuration file.