# Attributes

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Reflection and Metadata
Source: https://www.devtutorials.ai/learn/advanced-csharp/reflection-and-attributes/attributes

> Label code with attributes: built-in attributes, Obsolete, Conditional, caller information, parameters, custom attributes, targets, reading them with reflection and validation.

## Introduction to Attributes

Think about the labels on a parcel.

The parcel holds the item. The labels say **how to handle it**: "Fragile", "This side up", "Keep dry". The labels do not change what is inside. They give instructions to whoever handles the parcel.

**Attributes** are labels for your code. You put them in square brackets above a class, method or property:

```text
[Obsolete("Use SendAsync instead")]
public void Send() { ... }
```

The attribute does not change what `Send` does. It tells someone something about it. Here, it tells the **compiler** to warn anyone who still calls `Send`.

**In simple words:** an attribute is a sticky note on your code. Different tools read these notes and do different things based on what they say.

Who reads these labels?

- **The compiler** reads labels like `[Obsolete]` and `[Conditional]`. It warns you or changes the build based on the label.
- **Frameworks and libraries** read labels like `[Required]` or `[JsonPropertyName]`. They use the label to handle your data correctly.
- **Your code** can read labels too. You learned how to do this with reflection in the previous lesson.

**Easy Way to Remember:** an attribute is a **sticky note** on your code. It changes nothing by itself. It matters only when someone reads it.

## What You Will Learn

In this lesson, you will learn:

1. How to apply attributes, and where they can go.
2. The most useful built-in attributes.
3. `[Obsolete]`, `[Conditional]` and caller information attributes.
4. How attribute parameters work.
5. How to create your own custom attributes.
6. How to control where attributes can be placed with `[AttributeUsage]`.
7. How to read attributes using reflection.
8. How to build simple validation using attributes.

All runnable examples on this page run in your browser. A few examples cause a **compile error on purpose**, to show what an attribute does. Those are shown as plain code, because a program that does not compile cannot run.

## Start With a Working Example

Press **Run**.

```csharp runnable title="Labels that code can read"
using System;
using System.Reflection;

foreach (var property in typeof(SignUpForm).GetProperties())
{
    var label = property.GetCustomAttribute<DisplayLabelAttribute>();
    string text = label?.Text ?? property.Name;
    Console.WriteLine($"{text}: {property.GetValue(new SignUpForm())}");
}

class SignUpForm
{
    [DisplayLabel("Full name")]
    public string Name { get; set; } = "Asha Rao";

    [DisplayLabel("Email address")]
    public string Email { get; set; } = "asha@example.com";

    public int Age { get; set; } = 29;
}

class DisplayLabelAttribute : Attribute
{
    public string Text { get; }
    public DisplayLabelAttribute(string text) => Text = text;
}
```

The class stores the label. The loop reads the label and prints it. `Age` has no label, so the loop just prints the property name instead.

This is the main idea: write a label once, then read it and use it in many places.

### Try This

Add `[DisplayLabel("Your age")]` above `Age` and run again. The output changes, and the loop did not change at all.

## Applying Attributes

You apply an attribute by writing its name in square brackets, directly **before** the item it describes.

```text
[Serializable]
public class Invoice { }
```

Three rules about the syntax:

- **The `Attribute` suffix is optional.** The class is called `ObsoleteAttribute`, but you can write `[Obsolete]`.
- **You can stack attributes.** Write them on separate lines, or put several in one bracket separated by commas: `[Serializable, Obsolete]`.
- **Brackets can take arguments,** like a method call: `[Obsolete("Use V2")]`.

```csharp runnable title="Stacking attributes"
using System;
using System.Diagnostics;

var status = OrderStatus.Paid | OrderStatus.Shipped;
Console.WriteLine(status);
Console.WriteLine(status.HasFlag(OrderStatus.Shipped));

var point = new GpsPoint { Lat = 13.08, Lng = 80.27 };
Console.WriteLine($"{point.Lat}, {point.Lng}");

[Flags]
enum OrderStatus { None = 0, Paid = 1, Packed = 2, Shipped = 4 }

[Serializable, DebuggerDisplay("({Lat}, {Lng})")]
class GpsPoint
{
    public double Lat { get; set; }
    public double Lng { get; set; }
}
```

`[Flags]` tells .NET: "These enum values can be combined". That is why `ToString()` prints `Paid, Shipped` instead of the number `5`. Try combining them without `[Flags]` and you get just a number.

`[DebuggerDisplay]` tells the Visual Studio debugger: "Show this text when the user looks at this object in the debugger". It does not affect what your program does when it runs. Not every attribute matters to the running program.

## Built-in Attributes

.NET includes hundreds of attributes. You will see these ones most often.

| Attribute | Who reads it | What it does |
|---|---|---|
| `[Obsolete]` | Compiler | Warns, or errors, when old code is used |
| `[Conditional("X")]` | Compiler | Removes calls unless symbol `X` is defined |
| `[CallerMemberName]` and friends | Compiler | Fills in the caller's name, file or line number |
| `[Flags]` | Runtime | Lets enum values combine and print clearly |
| `[Serializable]` | Older serializers | Marks a type as safe to serialize |
| `[DebuggerDisplay]` | Debugger | Changes what the debugger shows |
| `[Required]`, `[Range]`, `[EmailAddress]` | Validation code | Rules for valid data |
| `[JsonPropertyName]`, `[JsonIgnore]` | `System.Text.Json` | Control how JSON is written |

The next three sections explain the compiler attributes in detail.

## Obsolete Attribute

Code changes over time. Sometimes a method is replaced by a better one, but you cannot delete the old method yet, because other code still calls it.

**`[Obsolete]`** marks the old member. Anyone who uses it gets a **compiler warning** with your message.

```csharp runnable title="Mark an old method as obsolete"
using System;

var sender = new SmsSender();
sender.Send("Your OTP is 4821");          // the editor underlines this call
sender.SendMessage("Your OTP is 4821", "+91 98400 12345");

class SmsSender
{
    [Obsolete("Use SendMessage(text, phone) instead. Send will be removed in v3.")]
    public void Send(string text) => Console.WriteLine($"(old) {text}");

    public void SendMessage(string text, string phone) =>
        Console.WriteLine($"To {phone}: {text}");
}
```

The program still runs, because a warning does not stop the build. Hover over the underlined `Send` in the editor to read the warning, `CS0618`.

### Turning the Warning Into an Error

Pass `true` as a second argument, and using the member becomes a **compile error** (`CS0619`). The program cannot be built until every caller is updated.

```text
[Obsolete("Use SendMessage(text, phone) instead.", true)]
public void Send(string text) { ... }

sender.Send("Hi");   // error CS0619: 'SmsSender.Send(string)' is obsolete:
                     // 'Use SendMessage(text, phone) instead.'
```

A common migration plan is: warning in version 2, error in version 3, removed in version 4. This gives callers time to update.

Always say **what to use instead** in the message. "This is obsolete" leaves the reader stuck. "Use SendMessage(text, phone) instead" tells them exactly what to do.

## Conditional Attribute

Some methods are only useful while developing, such as detailed logging or debug checks. You want the calls in your code, but you do not want them to run in the final release build.

**`[Conditional("SYMBOL")]`** does this. If `SYMBOL` is **not defined**, the compiler **erases every call** to the method. It even skips preparing the arguments.

A **symbol** is just a name. You turn it on with `#define` at the top of a file, or in the project settings. Visual Studio automatically turns on `DEBUG` in Debug builds only.

```csharp runnable title="Calls that disappear when a symbol is missing"
#define AUDIT
using System;
using System.Diagnostics;

Audit("Login: asha");
Trace("Loaded 3 orders");
Console.WriteLine("Main work done");

[Conditional("AUDIT")]
static void Audit(string message) => Console.WriteLine($"[AUDIT] {message}");

[Conditional("VERBOSE")]
static void Trace(string message) => Console.WriteLine($"[TRACE] {message}");
```

`AUDIT` is turned on at the top, so the `Audit` call runs and prints. `VERBOSE` is never turned on, so the `Trace` call disappears completely and prints nothing.

### Try This

Delete the first line, `#define AUDIT`, and run again. Now both calls disappear.

Two important rules for `[Conditional]` methods:

- The method must return **`void`**. A removed call cannot produce a return value.
- `#define` lines must come **before** everything else in the file, even `using` statements.

In this browser editor, `DEBUG` is **not** defined. A method marked `[Conditional("DEBUG")]` will never run here, even though it runs in Visual Studio when you press F5. That is why the example uses its own `AUDIT` symbol.

## Caller Information Attributes

When you write a log message, you often want to know **where** it came from. Typing the method name by hand is tedious, and it becomes wrong when you rename the method.

**Caller information attributes** tell the compiler: "Fill in this information for me automatically". You put them on **optional parameters**:

- `[CallerMemberName]` tells the compiler: "Write the method or property name here".
- `[CallerLineNumber]` tells the compiler: "Write the line number here".
- `[CallerFilePath]` tells the compiler: "Write the file path here".
- `[CallerArgumentExpression("param")]` tells the compiler: "Write the code text of the argument here".

The caller never passes these arguments. The compiler fills them in automatically each time the method is called.

```csharp runnable title="A logger that knows who called it"
using System;
using System.Runtime.CompilerServices;

var cart = new Cart();
cart.AddItem("Pen");
cart.Checkout();

static class Log
{
    public static void Info(string message,
        [CallerMemberName] string member = "",
        [CallerLineNumber] int line = 0)
    {
        Console.WriteLine($"[{member}, line {line}] {message}");
    }
}

class Cart
{
    public void AddItem(string name) => Log.Info($"Added {name}");
    public void Checkout() => Log.Info("Checking out");
}
```

`AddItem` and `Checkout` never type their own names. The compiler filled them in automatically.

### CallerArgumentExpression

This attribute captures the **code text** of an argument. It helps guard methods print exactly which check failed.

```csharp runnable title="Error messages that show the failing check"
using System;
using System.Runtime.CompilerServices;

int quantity = 0;
decimal price = 499m;

try
{
    Guard.IsTrue(price > 0);
    Guard.IsTrue(quantity > 0);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

static class Guard
{
    public static void IsTrue(bool condition,
        [CallerArgumentExpression(nameof(condition))] string expression = "")
    {
        if (!condition)
            throw new ArgumentException($"Check failed: {expression}");
        Console.WriteLine($"Passed: {expression}");
    }
}
```

The error message says exactly which check failed: `quantity > 0`. Without the attribute, you would only know that **some** check returned `false`, without knowing which one.

`[CallerMemberName]` is the classic helper for `INotifyPropertyChanged` in WPF and MAUI. The property setter calls `OnPropertyChanged()` with no argument, and the attribute automatically fills in the property name.

## Attribute Parameters

Attributes can have two kinds of parameters.

**1. Positional parameters** come from the attribute's **constructor**. You must provide them, and they go first in a specific order.

**2. Named parameters** set a public **property or field**. You can skip them if you want, and you write them as `Name = value`.

```text
[Range(1, 10, ErrorMessage = "Pick 1 to 10")]
//     ^  ^   ^ named: sets the ErrorMessage property
//     positional: the constructor's (minimum, maximum)
```

One important rule: attribute arguments must be **compile-time constants**. This means you must know the value when you write the code, not when the program runs. You **can** use:

- numbers, `bool`, `char` and strings,
- enum values,
- `typeof(SomeType)`,
- arrays of the above.

You **cannot** use `new DateTime(...)`, a `decimal` value, or any variable.

```csharp runnable title="Positional and named parameters"
using System;
using System.Reflection;

var info = typeof(PaymentService).GetCustomAttribute<ServiceInfoAttribute>()!;
Console.WriteLine($"Name:    {info.Name}");
Console.WriteLine($"Version: {info.Version}");
Console.WriteLine($"Owner:   {info.Owner}");
Console.WriteLine($"Tags:    {string.Join(", ", info.Tags)}");

[ServiceInfo("Payments", 2, Owner = "Ravi", Tags = new[] { "billing", "upi" })]
class PaymentService { }

class ServiceInfoAttribute : Attribute
{
    // Positional: from the constructor
    public string Name { get; }
    public int Version { get; }

    // Named: public settable properties
    public string Owner { get; set; } = "unknown";
    public string[] Tags { get; set; } = Array.Empty<string>();

    public ServiceInfoAttribute(string name, int version)
    {
        Name = name;
        Version = version;
    }
}
```

## Custom Attributes

A **custom attribute** is your own attribute class. You make one in three simple steps:

1. Create a class that **extends `System.Attribute`**.
2. End the name with **`Attribute`** so others know it is an attribute.
3. Add a constructor for the values you need and properties for optional values.

That is it. The attribute does not do anything by itself. It only helps when code reads it.

```csharp runnable title="A custom attribute for API permissions"
using System;
using System.Reflection;

foreach (var method in typeof(OrdersController).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
    var permission = method.GetCustomAttribute<RequiresRoleAttribute>();
    string who = permission == null ? "anyone" : permission.Role;
    Console.WriteLine($"{method.Name,-12} -> {who}");
}

class OrdersController
{
    public void List() { }

    [RequiresRole("Manager")]
    public void Refund() { }

    [RequiresRole("Admin")]
    public void DeleteAll() { }
}

[AttributeUsage(AttributeTargets.Method)]
class RequiresRoleAttribute : Attribute
{
    public string Role { get; }
    public RequiresRoleAttribute(string role) => Role = role;
}
```

A web framework does the same thing. When a user makes a request, it finds the method that will handle it, reads the permission label, and checks if the user has permission to run it.

## Attribute Targets

By default, an attribute can go almost anywhere. Usually that is wrong. A `[RequiresRole]` on an enum does not make sense.

**`[AttributeUsage]`** is an attribute you put on your attribute class. It says where your attribute is allowed. It controls three things:

| Setting | Meaning | Default |
|---|---|---|
| `AttributeTargets` | Where can this attribute go? | `All` |
| `AllowMultiple` | Can it appear more than once on the same item? | `false` |
| `Inherited` | Do child classes see the attribute from the parent class? | `true` |

Combine targets with `|`, for example `AttributeTargets.Class | AttributeTargets.Method`.

```csharp runnable title="AllowMultiple and Inherited"
using System;
using System.Reflection;

foreach (var tag in typeof(Laptop).GetCustomAttributes<TagAttribute>())
    Console.WriteLine($"Laptop tag: {tag.Name}");

// Inherited = true, so GamingLaptop sees the base class label
var cat = typeof(GamingLaptop).GetCustomAttribute<CategoryAttribute>();
Console.WriteLine($"GamingLaptop category: {cat?.Name}");

[Category("Electronics")]
[Tag("portable"), Tag("work"), Tag("sale")]
class Laptop { }

class GamingLaptop : Laptop { }

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class TagAttribute : Attribute
{
    public string Name { get; }
    public TagAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Class, Inherited = true)]
class CategoryAttribute : Attribute
{
    public string Name { get; }
    public CategoryAttribute(string name) => Name = name;
}
```

If you try to put `[RequiresRole]` on a class when it should only go on methods, the compiler stops you:

```text
[RequiresRole("Admin")]          // error CS0592: Attribute 'RequiresRole' is not allowed
class Settings { }               // here. It can only go on 'method'.
```

### Target Specifiers

Sometimes one line creates multiple things. An auto-property creates both a property **and** a hidden field. A method has a body **and** a return value.

When you need to pick which one gets the attribute, use a **target specifier**. Write it before the attribute name with a colon:

```text
[field: NonSerialized]           // the hidden backing field, not the property
public string Cache { get; set; }

[return: NotNull]                // the method's return value
public string GetName() { ... }

[assembly: AssemblyVersion("2.1.0")]   // the whole assembly, at the top of a file
```

## Reading Attributes With Reflection

Reflection has special methods to find attributes. You call them on `Type`, `PropertyInfo`, `MethodInfo` and similar classes.

| Method | What it does |
|---|---|
| `GetCustomAttribute<T>()` | Finds and returns the first attribute of type `T`, or nothing if there is no attribute |
| `GetCustomAttributes<T>()` | Finds and returns all attributes of type `T` |
| `IsDefined(typeof(T))` | Checks if an attribute exists (yes or no), without reading it |

To use these methods, add `using System.Reflection;` at the top of your file.

```csharp runnable title="Find every property marked as sensitive"
using System;
using System.Reflection;

var user = new UserProfile
{
    Name = "Priya",
    Email = "priya@example.com",
    Password = "s3cret!",
    Pan = "ABCDE1234F",
};

foreach (var property in typeof(UserProfile).GetProperties())
{
    bool hide = property.IsDefined(typeof(SensitiveAttribute));
    object? value = hide ? "******" : property.GetValue(user);
    Console.WriteLine($"{property.Name,-8}: {value}");
}

class UserProfile
{
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    [Sensitive] public string Password { get; set; } = "";
    [Sensitive] public string Pan { get; set; } = "";
}

[AttributeUsage(AttributeTargets.Property)]
class SensitiveAttribute : Attribute { }
```

This is how real programs keep data safe when logging. You mark a property `[Sensitive]` and the logger skips it.

Use `IsDefined` when you only care about yes or no. It is faster than `GetCustomAttribute`, because it does not create a new attribute object. `GetCustomAttribute` creates a new object each time, so if you need it many times, save it in a variable.

## Attribute-Based Validation

Now let's use what you learned. You will build a simple validator that checks objects by reading their attributes.

Each rule is a custom attribute with an `IsValid` method. The validator does not know about each rule separately. It just finds all the rule attributes and asks each one: "Is this value okay?"

```csharp runnable title="A tiny validation framework"
using System;
using System.Collections.Generic;
using System.Reflection;

var good = new Registration { Name = "Asha", Age = 29, Email = "asha@example.com" };
var bad = new Registration { Name = "", Age = 12, Email = "asha.example.com" };

Print("good", Validator.Validate(good));
Print("bad", Validator.Validate(bad));

static void Print(string label, List<string> errors)
{
    Console.WriteLine(errors.Count == 0 ? $"{label}: valid" : $"{label}: {errors.Count} problem(s)");
    foreach (string e in errors) Console.WriteLine($"  - {e}");
}

class Registration
{
    [NotEmpty] public string Name { get; set; } = "";
    [Between(18, 120)] public int Age { get; set; }
    [NotEmpty, Contains("@")] public string Email { get; set; } = "";
}

static class Validator
{
    public static List<string> Validate(object target)
    {
        var errors = new List<string>();
        foreach (var property in target.GetType().GetProperties())
        {
            object? value = property.GetValue(target);
            foreach (var rule in property.GetCustomAttributes<RuleAttribute>())
            {
                if (!rule.IsValid(value))
                    errors.Add($"{property.Name} {rule.Message}");
            }
        }
        return errors;
    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
abstract class RuleAttribute : Attribute
{
    public abstract string Message { get; }
    public abstract bool IsValid(object? value);
}

class NotEmptyAttribute : RuleAttribute
{
    public override string Message => "must not be empty";
    public override bool IsValid(object? value) => value is string s && s.Length > 0;
}

class BetweenAttribute : RuleAttribute
{
    private readonly int _min, _max;
    public BetweenAttribute(int min, int max) { _min = min; _max = max; }
    public override string Message => $"must be between {_min} and {_max}";
    public override bool IsValid(object? value) => value is int n && n >= _min && n <= _max;
}

class ContainsAttribute : RuleAttribute
{
    private readonly string _text;
    public ContainsAttribute(string text) => _text = text;
    public override string Message => $"must contain '{_text}'";
    public override bool IsValid(object? value) => value is string s && s.Contains(_text);
}
```

`GetCustomAttributes<RuleAttribute>()` finds every attribute that **is a** `RuleAttribute`. To add a new rule, you write one small class. The validator stays the same.

### The Built-in Version: Data Annotations

.NET comes with a validation framework built in, called `System.ComponentModel.DataAnnotations`. ASP.NET Core and Blazor use it automatically.

```csharp runnable title="Validation with DataAnnotations"
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

var form = new ContactForm { Name = "", Email = "not-an-email", Age = 15 };

var results = new List<ValidationResult>();
bool ok = Validator.TryValidateObject(form, new ValidationContext(form), results, validateAllProperties: true);

Console.WriteLine($"Valid: {ok}");
foreach (var r in results)
    Console.WriteLine($"  - {r.ErrorMessage}");

class ContactForm
{
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; } = "";

    [EmailAddress(ErrorMessage = "Email is not valid")]
    public string Email { get; set; } = "";

    [Range(18, 99, ErrorMessage = "Age must be 18 to 99")]
    public int Age { get; set; }
}
```

In real projects, use Data Annotations first. Only write your own attributes for checks the built-in ones do not handle.

## Where Attributes Are Used

- **Web APIs:** `[HttpGet]`, `[Route]`, `[Authorize]`, `[FromBody]` in ASP.NET Core.
- **Validation:** `[Required]`, `[Range]`, `[EmailAddress]` in forms and APIs.
- **JSON:** `[JsonPropertyName("first_name")]`, `[JsonIgnore]`.
- **Databases:** `[Key]`, `[Table("orders")]`, `[MaxLength(100)]` in Entity Framework.
- **Testing:** `[Fact]`, `[Theory]`, `[TestMethod]`.
- **Code generators:** `[GeneratedRegex]`, `[LoggerMessage]`, `[JsonSerializable]`.

## Common mistakes

| Mistake | Symptom | Fix |
|---|---|---|
| No code reads the attribute | Nothing happens | Read it with reflection |
| Variable in attribute arguments | `CS0182` error | Use a `const` or a literal value |
| Missing `using System.Reflection` | `CS1061` on `GetCustomAttribute` | Add the `using` statement |
| `[Conditional("DEBUG")]` in a Release build | Method never runs | Check which symbols are defined |
| Attribute placed on wrong kind of member | `CS0592` error | Check the `[AttributeUsage]` specification |
| Same attribute twice without `AllowMultiple` | `CS0579` duplicate attribute error | Set `AllowMultiple = true` |

## What to Remember

- An attribute is a label on code. Nothing happens until someone or something reads it.
- Put attributes in square brackets. The word `Attribute` at the end is optional.
- `[Obsolete]` warns people not to use your code. Add `true` to make it an error.
- `[Conditional("X")]` erases a method call if symbol `X` is not defined. The method must return `void`.
- Caller information attributes let the compiler fill in details like the method name or line number automatically.
- Positional parameters come from the constructor; named parameters set properties.
- Attribute arguments must be constants you know at write time, not variables.
- Write a custom attribute by making a class that extends `Attribute`. Use `[AttributeUsage]` to limit where it can go.
- Read attributes with `GetCustomAttribute<T>()` to get one, `GetCustomAttributes<T>()` to get all, or `IsDefined` for yes/no.
- Validation systems are just attributes with code that reads them using reflection.

Next: Dynamic Types
