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.
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.
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.
| Kind | How it is passed | Where it maps | Required? |
|---|---|---|---|
| Positional | By order, like ("Asha") | A constructor parameter | Yes, the constructor needs it |
| Named | By name, like Date = "..." | A public property or field | No, 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
Steps
Write [Author]
Apply on a class
Positional
Run the constructor
Named
Set public props
Object built
Note is ready
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.
| Member | What it controls | Our choice |
|---|---|---|
AttributeTargets | Which code elements accept it | Classes and methods only |
AllowMultiple | Can you attach it more than once? | Yes, true |
Inherited | Do 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.
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.
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
Steps
Get properties
List all props
Find [RequiredField]
Skip if missing
Check value
Empty or not
Collect errors
Build message list
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.Jsonuses[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
AllowMultipleat 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 anAttributesuffix. - 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
GetCustomAttributesor the genericGetCustomAttribute<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
- Create custom attributes (Microsoft Learn)
- Tutorial: Define and read custom attributes (Microsoft Learn)
- Attributes and reflection overview (Microsoft Learn)
- Writing custom attributes (.NET, Microsoft Learn)
Related Posts
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.
How to Apply Functional Programming in C#: A Beginner's Guide
Learn functional programming in C# the simple way: pure functions, immutability, records, LINQ, pattern matching, and composition with friendly examples.
New Features in C# 13: A Friendly Beginner's Guide
Learn the new features in C# 13 with simple words, real-life examples, diagrams, and code you can read in minutes. Great for beginners.
Mastering Exception Handling in C#: A Comprehensive Guide
Learn C# exception handling the friendly way: try, catch, finally, custom exceptions, filters, throw vs throw ex, and real best practices for .NET 10.
Getting Started with C# Records: A Beginner's Friendly Guide
Learn C# records the easy way: value equality, with expressions, positional syntax, and record struct, explained with simple real-life examples.