Back to Blog
C#

C# Attributes: Syntax, Usage, and Runtime Behavior

c# attributes: Learn the syntax, targets, and runtime behavior of C# attributes, including creating custom attributes and retrieving them with reflection.

C# AttributesReflectionCustom AttributesMetadataDependency InjectionValidation
Diagram showing C# code elements tagged with metadata labels, illustrating how attributes attach to declarations.

C# attributes are declarative tags that attach metadata to code elements such as classes, methods, properties, and parameters. This metadata sits in the assembly metadata and can be read at runtime via reflection or used by tools and the compiler to influence behavior. When you apply an attribute, you are not adding logic to the element itself; you are adding structured data that other code can inspect and act on.

What C# Attributes Actually Do

An attribute in C# is an instance of a class derived from System.Attribute. When you place [Obsolete] above a method, the compiler adds an ObsoleteAttribute instance to the method's metadata. The attribute class itself contains no behavior unless you write code that reads that instance and reacts to it.

For example, the [Serializable] attribute marks a class as serializable. The BinaryFormatter (or other serializers) checks for the presence of that attribute at runtime. The attribute does nothing by itself; it is a marker that triggers behavior elsewhere.

Attributes are not compiled into the method body. They are stored as metadata records, which makes them inspectable without executing the containing code. This separation is key to understanding how they behave in frameworks like ASP.NET Core, where attributes such as [HttpGet] or [Authorize] are read by the framework's request pipeline.

Attribute Targets and Valid Usage

Every attribute has a target. The target defines which code elements the attribute can be applied to. In C#, you specify the allowed targets using the AttributeUsage attribute on the attribute class. Common targets include assembly, module, type (class, struct, enum, delegate), method, property, field, event, interface, parameter, and return value.

Consider this custom attribute that is only valid on methods:

[AttributeUsage(AttributeTargets.Method)] public sealed class LogExecutionAttribute : Attribute { public string Level { get; set; } }

The AttributeUsage attribute also controls whether the attribute can be inherited by derived classes and whether it can be applied multiple times to the same element.

TargetExampleCommon Use
Class[Serializable]Marking a type for serialization
Method[HttpGet]Routing an action to an HTTP verb
Property[Required]Validating a model property
Parameter[FromBody]Indicating how to bind a parameter
Assembly[AssemblyTitle]Storing assembly metadata

If you apply an attribute to an invalid target, the compiler produces an error. This is because the attribute class's AttributeUsage is checked at compile time.

Creating a Custom C# Attribute

To create a custom attribute, define a class that inherits from System.Attribute. The class name conventionally ends with Attribute, but when you apply it, you can omit the suffix. The constructor parameters become the positional arguments of the attribute, and public read-write properties become named arguments.

Here is a custom attribute that stores an author name and a version:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class AuthorAttribute : Attribute { public string Name { get; } public string Version { get; } public AuthorAttribute(string name, string version) { Name = name; Version = version; } }

You can apply it like this:

[Author("Ada", "1.0")] public class SampleService { [Author("Grace", "2.0")] public void Run() { } }

The AuthorAttribute instance is created only when reflection reads it. This is important: attribute constructors do not run when the attribute is applied, but when the attribute is retrieved via reflection.

Reading Attributes with Reflection

To make attributes useful, you need to read them. Reflection APIs require the System.Reflection namespace. The GetCustomAttributes method extracts the attribute instances from a member's metadata.

The following code retrieves the AuthorAttribute from a method and reads its properties:

using System; using System.Linq; using System.Reflection; MethodInfo method = typeof(SampleService).GetMethod(nameof(SampleService.Run)); AuthorAttribute? attr = method.GetCustomAttributes(typeof(AuthorAttribute), inherit: true) .FirstOrDefault() as AuthorAttribute; if (attr != null) { Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}"); }

The inherit: true parameter controls whether attributes defined on base classes or base methods are included. If the attribute class has Inherited = false in its AttributeUsage, then inherit: true will not return it for derived elements.

Reflection is the standard way to inspect attributes, but it has a cost. Every GetCustomAttributes call allocates an array and invokes the attribute constructor. If you call it frequently, that overhead becomes noticeable. For performance-sensitive paths, you can cache the results or use source generators that generate attribute-reading code at compile time, avoiding reflection entirely.

Attribute Constructors, Named Arguments, and Equality

Attribute constructors accept only a specific set of types: primitives, strings, Type, enums, and one-dimensional arrays of those types. You cannot pass a custom class or List<T> as a positional or named argument because the metadata format does not support arbitrary objects. This constraint is a direct consequence of how attributes are stored in metadata.

Named arguments are property or field assignments you specify in the application:

[LogExecution(Level = "Warning", SkipOutput = true)] public void Process() { }

Here Level is a property, and SkipOutput could be a field. Named arguments are optional; you can omit them, and the property will have its default value.

Attribute equality is not built in. If you compare two attribute instances using Equals, you get reference equality unless you override Equals in the attribute class. For most scenarios, you compare the property values you care about rather than the attribute instance itself.

Runtime Behavior and the Role of Reflection

Attributes have no direct runtime behavior. The runtime does not automatically execute anything because an attribute is present. Instead, the runtime provides reflection APIs that let you discover attributes and then act on them. Frameworks such as ASP.NET Core, Entity Framework Core, and the .NET configuration system rely heavily on attributes to wire up behaviors.

The [Authorize] attribute in ASP.NET Core, for example, does not perform authorization itself. The authorization middleware checks the endpoint metadata for the presence of AuthorizeAttribute and its policy names. If that attribute is missing, the middleware may allow the request. Similarly, [Required] in System.ComponentModel.DataAnnotations is used by the model validation system, not by the property itself.

This design means the effect of an attribute depends entirely on the code that reads it. If no code reads an attribute, it has no effect at runtime. The compiler may use some attributes for warnings or interop, but for custom attributes, reflection is the only bridge from metadata to behavior.

Attribute Inheritance and Overriding

AttributeUsage.Inherited affects how attributes are discovered on types that derive from a base class. If Inherited = true (the default for most attributes), then an attribute placed on a base class will appear when you query attributes of a derived class. This behavior mirrors method inheritance.

[Author("Grace", "1.0")] public class BaseService { } public class DerivedService : BaseService { }

When you call typeof(DerivedService).GetCustomAttributes(inherit: true), you get the AuthorAttribute from BaseService. If you set Inherited = false, the derived class will not expose the base attribute.

There is an important distinction between inheriting the attribute and overriding it. If a derived class applies the same attribute type, the derived attribute is returned instead of the base one when you use inherit: true. This is similar to how a derived method can hide a base method.

Be careful with AllowMultiple. If you set AllowMultiple = true, you can apply the same attribute multiple times to the same element, and reflection returns an array containing all instances. This is useful for tags or validation rules that repeat.

Performance and Maintainability Considerations

Reflection-based attribute reading has a measurable cost because it allocates attribute instances on each call. The first call might also trigger type initialization and metadata lookup. If you read attributes in a hot code path, consider caching the results.

A basic caching approach:

private static readonly ConcurrentDictionary<MemberInfo, AuthorAttribute?> Cache = new(); public static AuthorAttribute? GetAuthor(MemberInfo member) { return Cache.GetOrAdd(member, m => m.GetCustomAttributes(inherit: true) .OfType<AuthorAttribute>() .FirstOrDefault()); }

Caching by MemberInfo ensures that the attribute is retrieved once and reused. However, this cache is global and will hold references to the MemberInfo objects forever. In a long-running application, that could prevent garbage collection of types loaded dynamically. Use such caches sparingly or with weak references.

From a maintainability perspective, attributes are a form of declarative configuration. They keep related metadata close to the code they affect, but they obscure behavior when the logic that reads them is hidden in a framework. When you add a custom attribute, you are also adding the responsibility of documenting what code will read it and what effect it will have. Without that, another developer may apply the attribute expecting behavior that never happens.

Using Attributes for Validation Without Polluting Business Logic

One common use of custom attributes is to externalize validation rules. Instead of writing if statements inside every method, you can decorate properties with validation attributes and centralize the validation logic in a service that reads those attributes.

Define a custom attribute:

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

Apply it to a model:

public class UserInput { [MaxLength(20)] public string Username { get; set; } = string.Empty; }

Then a validation engine reflects over the model's properties:

public static bool Validate(object instance) { foreach (PropertyInfo prop in instance.GetType().GetProperties()) { MaxLengthAttribute? maxLen = prop.GetCustomAttribute<MaxLengthAttribute>(); if (maxLen == null) continue; string? value = prop.GetValue(instance) as string; if (value != null && value.Length > maxLen.Length) { return false; } } return true; }

The validation logic lives in one place, and each property declares its own constraint. This reduces duplication compared to writing if checks in every consumer of UserInput. It also makes the constraints visible where the data is defined.

A downside is that this validation is opt-in: nothing forces the caller to invoke Validate. Unlike compile-time checks, missing validation calls fail silently. This is why many teams combine attributes with source generators to produce compile-time checks, or they use immutable types with constraints enforced in the factory methods.

A common pitfall is using attributes as a substitute for proper design. If you find yourself storing executable logic inside an attribute's property values, step back. Attributes work best for static metadata that is known at compile time. For dynamic behavior, consider a strategy pattern or a separate service instead.

c# attributes: Practical Usage and Code Examples | RYUSLOG DEV