Skip to main content
SEMastery
.NET Corebeginner

Creating Custom Attributes in C#: A Beginner's Guide

Learn to create custom attributes in C# from scratch. Use AttributeUsage, attach extra info to your code, and read it back with reflection. Simple examples.

11 min readUpdated February 17, 2026

Think about a school library. Every book has a small card or sticker inside the front cover. The sticker does not change the story in the book. But it tells the librarian useful things: who wrote it, which shelf it belongs to, and whether it can leave the building. The librarian reads the sticker to decide what to do.

Custom attributes in C# are exactly like that sticker. You attach a little note to a class, a method, or a property. The note does not change what the code does. But later, other code can read the note and decide how to behave. That is the whole idea, and it is simpler than it sounds.

In this post you will learn what attributes are, how to make your own, how to control where they can be used, and how to read them back at runtime. We will use modern C# 14 (which ships with .NET 10, the current LTS release), but everything here works in older versions too.

What an attribute really is

An attribute is just a class. A special kind of class, but still a class. When you write [Serializable] or [Obsolete] above some code, you are attaching an instance of a class to that code. .NET ships with many built-in attributes, and you have probably used some without thinking about it.

The note you attach is called metadata. Metadata means "data about your code." It is information that sits next to your code but is not the code itself.

An attribute is a note attached to a code element

Here is the key thing to hold in your head. Attaching an attribute does almost nothing on its own. It only becomes useful when some other code goes looking for it. The library sticker is useless until the librarian reads it. Same here.

A first custom attribute

Let us make our own sticker. We want to mark classes with the name of the person who wrote them. We will call it AuthorAttribute.

To create a custom attribute, you write a class that inherits from System.Attribute. By convention the name ends with Attribute. When you use it, you can drop that ending.

using System;
 
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Date { get; set; } = "";
 
    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

Now we can stick this note onto any class.

[Author("Asha", Date = "2026-06-10")]
public class SalesReport
{
    public decimal Total { get; set; }
}

Notice two things. First, we wrote [Author(...)] and not [AuthorAttribute(...)]. Both work, but the short form reads nicer, and the compiler adds the suffix for you. Second, "Asha" goes through the constructor, while Date is set by name. That is an important rule, so let us slow down on it.

Positional and named arguments

Attribute values come in two flavours. Understanding the difference removes most of the confusion beginners have.

KindHow it is passedWhere it mapsRequired?
PositionalBy order, like ("Asha")A constructor parameterYes, the constructor needs it
NamedBy name, like Date = "..."A public property or fieldNo, optional

Positional arguments are the ones your constructor asks for. They are required, because you cannot build the object without them. Named arguments come after the positional ones and set public properties or fields. They are optional. In our example Name is positional and Date is named.

How attribute values are filled

Write [Author]
Positional
Named
Object built

Steps

1

Write [Author]

Apply on a class

2

Positional

Run the constructor

3

Named

Set public props

4

Object built

Note is ready

Constructor first, then properties

One more rule worth memorising: attribute values must be compile-time constants. You can use strings, numbers, booleans, enums, Type objects, and arrays of those. You cannot pass the result of a method call or a value from a database. The note is baked in when you compile, so it has to be known then.

Controlling where an attribute can go with AttributeUsage

Right now our [Author] can be placed on anything: classes, methods, properties, even a single parameter. That is messy. We probably only want it on classes and methods. C# gives us a way to set rules, and the way is itself an attribute: [AttributeUsage].

You place [AttributeUsage] on top of your attribute class. It has one positional argument and two useful named ones.

using System;
 
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Method,
    AllowMultiple = true,
    Inherited = false)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Date { get; set; } = "";
 
    public AuthorAttribute(string name) => Name = name;
}

Let us read this slowly.

MemberWhat it controlsOur choice
AttributeTargetsWhich code elements accept itClasses and methods only
AllowMultipleCan you attach it more than once?Yes, true
InheritedDo child classes inherit the note?No, false

The | symbol in AttributeTargets.Class | AttributeTargets.Method is a bitwise OR. It just means "class or method." You can combine as many targets as you like this way. If you wanted it everywhere, you could write AttributeTargets.All.

Because we set AllowMultiple = true, a class can now have several authors, which is handy for code many people touched.

[Author("Asha")]
[Author("Ravi", Date = "2026-06-11")]
public class SalesReport
{
    public decimal Total { get; set; }
}

If AllowMultiple were left at its default of false, the second [Author] line would be a compile error. The compiler enforces these rules for you, which is one of the nicest parts of attributes.

AttributeUsage sets the rules the compiler checks

Reading attributes back with reflection

So far we have only attached notes. Nobody is reading them yet. To read attributes at runtime, we use reflection. Reflection is the part of .NET that lets your code look at itself: list a class's methods, check its properties, and find its attributes.

The main method we need is GetCustomAttributes. You call it on a Type (for a class) or on a MethodInfo (for a method). It hands back the attribute objects that were attached.

Here is code that reads the authors off our SalesReport class.

using System;
using System.Reflection;
 
Type type = typeof(SalesReport);
 
object[] notes = type.GetCustomAttributes(typeof(AuthorAttribute), inherit: false);
 
foreach (AuthorAttribute author in notes)
{
    Console.WriteLine($"Author: {author.Name}, Date: {author.Date}");
}

A cleaner, modern way uses the generic helper from System.Reflection, which avoids casting:

using System.Reflection;
 
foreach (AuthorAttribute author in type.GetCustomAttributes<AuthorAttribute>())
{
    Console.WriteLine($"Author: {author.Name}");
}

When this runs, .NET finds the [Author] notes, builds the AuthorAttribute objects (running their constructors), fills in the named values, and gives them to you as normal objects. From that point on, they are just plain C# objects. You read author.Name like any other property.

The journey from a note to a usable object

A real example: a simple validator

Let us put it all together with something closer to real life. Imagine you have a form object, and you want to mark which fields are required. Built-in validation already exists in .NET, but building a tiny version yourself is the best way to understand how the big libraries work under the hood.

First, the attribute. It carries no data, just the fact "this field must be filled."

using System;
 
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RequiredFieldAttribute : Attribute
{
    public string Message { get; set; } = "This field is required.";
}

Now a class that uses it:

public class SignUpForm
{
    [RequiredField(Message = "Please enter your name.")]
    public string Name { get; set; } = "";
 
    [RequiredField]
    public string Email { get; set; } = "";
 
    public string? Nickname { get; set; }
}

Finally, a validator that walks every property, checks for the attribute, and complains if a required field is empty:

using System.Reflection;
 
public static class Validator
{
    public static List<string> Validate(object form)
    {
        var errors = new List<string>();
 
        foreach (PropertyInfo prop in form.GetType().GetProperties())
        {
            var rule = prop.GetCustomAttribute<RequiredFieldAttribute>();
            if (rule is null)
                continue;
 
            var value = prop.GetValue(form) as string;
            if (string.IsNullOrWhiteSpace(value))
                errors.Add(rule.Message);
        }
 
        return errors;
    }
}

Run it on an empty form and you get back two friendly error messages. The Nickname property has no attribute, so the validator skips it. This is the exact pattern that model validation in ASP.NET Core uses, just with more features.

How the validator works

Get properties
Find [RequiredField]
Check value
Collect errors

Steps

1

Get properties

List all props

2

Find [RequiredField]

Skip if missing

3

Check value

Empty or not

4

Collect errors

Build message list

Loop, check the note, report

Where attributes are used in the real world

You already meet attributes every day in .NET, often without writing one yourself. Seeing the pattern helps the idea stick.

  • ASP.NET Core: [HttpGet], [Route], and [Authorize] tell the framework how to handle a request and who is allowed in.
  • Validation: [Required], [EmailAddress], and [StringLength] describe rules for form data.
  • Serialization: System.Text.Json uses [JsonPropertyName] to rename fields in JSON. (This is the recommended built-in serializer in modern .NET, so you do not need an extra package for most work.)
  • Testing: xUnit uses [Fact] and [Theory] to mark test methods.
  • Compiler hints: [Obsolete] makes the compiler warn anyone who uses old code.

In every case the pattern is the same. Someone wrote an attribute. You attach it. A framework reads it with reflection and acts. Once you see that loop, the whole .NET ecosystem feels less magical and more like a set of well-organised stickers.

A quick word on performance

Reflection is powerful but slower than ordinary method calls. Reading an attribute means searching metadata, which takes real time. For a one-off check at startup, this does not matter at all. But if you read the same attribute thousands of times in a tight loop, the cost adds up.

The fix is simple: read once, store the result, and reuse it. A static dictionary that maps a Type to its attributes is a common trick. Newer .NET versions also offer source generators, which do attribute-style work at compile time and produce fast code with no runtime reflection. That is an advanced topic for later, but it is good to know the option exists when speed truly matters.

Common mistakes to avoid

A few traps catch almost every beginner. Knowing them ahead of time saves an afternoon of confusion.

  • Forgetting to inherit from System.Attribute. Without it, your class is not an attribute and cannot be used in [brackets].
  • Trying to pass a non-constant value, like [Author(GetName())]. Attribute arguments must be known at compile time.
  • Expecting the note to do something by itself. It does nothing until reflection reads it. The attribute is data, not behaviour.
  • Leaving AllowMultiple at its default and then trying to stack two of the same attribute. The compiler will stop you.
  • Reading attributes in a hot loop without caching, then wondering why the app is slow.

Quick recap

  • An attribute is a small note (metadata) you attach to code. It does not change what the code does.
  • You create one by writing a class that inherits from System.Attribute, named with an Attribute suffix.
  • Positional arguments map to constructor parameters and are required. Named arguments set public properties and are optional.
  • All attribute values must be compile-time constants like strings, numbers, enums, or Type.
  • [AttributeUsage] controls where your attribute can go (AttributeTargets), whether it can repeat (AllowMultiple), and whether children inherit it (Inherited).
  • Reflection reads attributes back at runtime with GetCustomAttributes or the generic GetCustomAttribute<T>().
  • Frameworks like ASP.NET Core, JSON serialization, and test runners all follow this same attach-then-read pattern.
  • Reflection is slower than normal code, so cache results or consider source generators for hot paths.

References and further reading

Related Posts