How to Define Custom Attributes in C#

Introduction

It's tempting to hard-code framework behaviors directly into your classes. It works until you need to change how validation runs, how logging happens, or how serialization behaves across dozens of classes. Suddenly, you're editing every file to make one conceptual change.

Custom attributes provide a cleaner pattern. They let you declare behavior through metadata rather than procedural code. ASP.NET uses attributes to mark API routes. Validation frameworks use them to specify rules. Testing frameworks use them to identify test methods. Your code describes what it needs, and frameworks discover that metadata through reflection.

You'll learn how to create your own attribute classes, control where they can be used, read them through reflection, and build practical systems that leverage attributes for validation, logging, and configuration.

Creating Your First Custom Attribute

A custom attribute is a class that inherits from System.Attribute. The class name typically ends with "Attribute", though you can omit that suffix when applying it. You define properties or constructor parameters to hold the attribute's data.

The simplest custom attribute has no parameters. It serves as a marker that flags a class or method for special treatment. More complex attributes carry data that influences how the attributed element behaves.

AuthorAttribute.cs
using System;

// Define a custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
                AllowMultiple = true)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Date { get; set; }
    public int Version { get; set; }

    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

// Apply the attribute
[Author("Alice Johnson", Date = "2025-11-04", Version = 1)]
public class Calculator
{
    [Author("Bob Smith", Version = 2)]
    public int Add(int a, int b)
    {
        return a + b;
    }
}

The AttributeUsage attribute controls where your custom attribute can be applied. AttributeTargets.Class means it can decorate classes, while AttributeTargets.Method allows it on methods. The pipe operator combines multiple targets. AllowMultiple permits applying the same attribute multiple times to one element.

The Name property uses a constructor parameter, making it required. Date and Version use property initializers, making them optional named parameters. This pattern gives you flexibility in how developers apply your attribute.

Controlling Attribute Usage

The AttributeUsage attribute determines where your custom attribute is valid and how it behaves with inheritance. You specify targets using the AttributeTargets enumeration, which includes Class, Method, Property, Field, Parameter, and more.

Setting AllowMultiple controls whether the same attribute can appear multiple times on one element. Setting Inherited determines whether derived classes automatically receive the attribute from their base class.

ValidationAttributes.cs
using System;

// Only valid on properties, single use, inherited by subclasses
[AttributeUsage(AttributeTargets.Property,
                AllowMultiple = false,
                Inherited = true)]
public class RequiredAttribute : Attribute
{
    public string ErrorMessage { get; set; } = "This field is required";
}

// Valid on properties, multiple uses allowed
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class ValidationRuleAttribute : Attribute
{
    public string RuleName { get; }
    public string Pattern { get; set; }

    public ValidationRuleAttribute(string ruleName)
    {
        RuleName = ruleName;
    }
}

// Example usage
public class User
{
    [Required(ErrorMessage = "Username cannot be empty")]
    public string Username { get; set; }

    [ValidationRule("Email", Pattern = @"^\S+@\S+\.\S+$")]
    [ValidationRule("Length", Pattern = @"^.{5,50}$")]
    public string Email { get; set; }
}

The Required attribute can only appear once per property, which prevents conflicting error messages. The ValidationRule attribute allows multiple instances, letting you stack validation rules on a single property. This design gives you flexibility while preventing nonsensical attribute combinations.

Reading Attributes with Reflection

After defining custom attributes, you need to read them at runtime. The reflection API provides methods like GetCustomAttribute and GetCustomAttributes to retrieve attribute instances from types, methods, properties, and other elements.

Reading attributes involves getting the Type or MemberInfo for the element, then calling reflection methods to access the attached attributes. The attribute constructor and properties execute when reflection instantiates them.

AttributeReader.cs
using System;
using System.Reflection;

public class AttributeReader
{
    public static void ReadClassAttributes()
    {
        var type = typeof(Calculator);

        // Read single attribute
        var author = type.GetCustomAttribute<AuthorAttribute>();
        if (author != null)
        {
            Console.WriteLine($"Author: {author.Name}");
            Console.WriteLine($"Date: {author.Date}");
            Console.WriteLine($"Version: {author.Version}");
        }

        // Read multiple attributes
        var allAuthors = type.GetCustomAttributes<AuthorAttribute>();
        foreach (var a in allAuthors)
        {
            Console.WriteLine($"Contributor: {a.Name}");
        }
    }

    public static void ReadPropertyAttributes()
    {
        var type = typeof(User);
        var emailProperty = type.GetProperty("Email");

        var rules = emailProperty?.GetCustomAttributes<ValidationRuleAttribute>();
        if (rules != null)
        {
            foreach (var rule in rules)
            {
                Console.WriteLine($"Rule: {rule.RuleName}");
                Console.WriteLine($"Pattern: {rule.Pattern}");
            }
        }
    }
}

GetCustomAttribute returns a single attribute or null if not found. GetCustomAttributes returns all matching attributes as an enumerable. Always check for null before accessing attribute properties, since the attribute might not be present.

Trimming and Native AOT Considerations: When publishing trimmed or Native AOT applications, reflection may require annotations like [DynamicallyAccessedMembers] or [DynamicDependency]. Libraries using reflection should annotate types and members accordingly. See Microsoft's trimming documentation at https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/

Building a Simple Validation Framework

Let's build a practical validation system using custom attributes. This demonstrates how frameworks like ASP.NET Data Annotations work internally. You'll define validation attributes and create a validator that reads them through reflection.

ValidationFramework.cs
using System;
using System.Collections.Generic;
using System.Reflection;

[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute : Attribute
{
    public int Min { get; }
    public int Max { get; }
    public string ErrorMessage { get; set; }

    public RangeAttribute(int min, int max)
    {
        Min = min;
        Max = max;
        ErrorMessage = $"Value must be between {min} and {max}";
    }
}

[AttributeUsage(AttributeTargets.Property)]
public class StringLengthAttribute : Attribute
{
    public int MaxLength { get; }
    public string ErrorMessage { get; set; }

    public StringLengthAttribute(int maxLength)
    {
        MaxLength = maxLength;
        ErrorMessage = $"Length cannot exceed {maxLength} characters";
    }
}

public class Validator
{
    public static List<string> Validate(object obj)
    {
        var errors = new List<string>();
        var type = obj.GetType();

        foreach (var prop in type.GetProperties())
        {
            var value = prop.GetValue(obj);

            // Check Required
            var required = prop.GetCustomAttribute<RequiredAttribute>();
            if (required != null && (value == null ||
                (value is string s && string.IsNullOrEmpty(s))))
            {
                errors.Add(required.ErrorMessage);
            }

            // Check Range
            var range = prop.GetCustomAttribute<RangeAttribute>();
            if (range != null && value is int intValue)
            {
                if (intValue < range.Min || intValue > range.Max)
                {
                    errors.Add(range.ErrorMessage);
                }
            }

            // Check StringLength
            var length = prop.GetCustomAttribute<StringLengthAttribute>();
            if (length != null && value is string strValue)
            {
                if (strValue.Length > length.MaxLength)
                {
                    errors.Add(length.ErrorMessage);
                }
            }
        }

        return errors;
    }
}

// Example usage
public class Product
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(50, ErrorMessage = "Name too long")]
    public string Name { get; set; }

    [Range(1, 1000, ErrorMessage = "Price must be between 1 and 1000")]
    public int Price { get; set; }
}

The Validator iterates through all properties, reads validation attributes, and checks values against rules. Each attribute type has specific validation logic. This pattern separates validation rules from business logic, making your domain classes cleaner.

Try It Yourself

Here's a complete example that demonstrates creating and using custom attributes. This program defines attributes, applies them to classes, and reads them through reflection.

Program.cs
using System;
using System.Collections.Generic;

var product = new Product
{
    Name = "Laptop",
    Price = 500
};

var errors = Validator.Validate(product);

if (errors.Count == 0)
{
    Console.WriteLine("Validation passed!");
}
else
{
    Console.WriteLine("Validation errors:");
    foreach (var error in errors)
    {
        Console.WriteLine($"  - {error}");
    }
}

// Test with invalid data
var invalidProduct = new Product
{
    Name = "",
    Price = 1500
};

errors = Validator.Validate(invalidProduct);
Console.WriteLine($"\nFound {errors.Count} validation errors:");
foreach (var error in errors)
{
    Console.WriteLine($"  - {error}");
}
Project.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Output:

Validation passed!

Found 2 validation errors:
  - Product name is required
  - Price must be between 1 and 1000

Run this with dotnet run. The validator reads attributes and applies rules automatically. Add more attributes or properties to see how the system scales without modifying the validator code.

Security and Trimming Considerations

Custom attributes often rely on reflection, which introduces security and deployment considerations. When using reflection, you need to be aware of trimming and Native AOT compatibility.

Trimming removes unused code from published applications to reduce size. The trimmer may remove types, methods, or properties that you access only through reflection. You must annotate reflection-heavy code with attributes like DynamicallyAccessedMembers to preserve necessary metadata.

Native AOT compilation eliminates the just-in-time compiler for faster startup and smaller memory footprint. However, it restricts reflection significantly. Attributes used only for compile-time code generation work fine, but runtime reflection requires careful annotation or may not work at all.

For attribute-based systems, consider these alternatives. Use source generators to process attributes at compile time instead of runtime. Cache reflection results in static fields during application startup to minimize performance impact. For frequently validated objects, generate validators at build time rather than using reflection on every validation call.

Common Pitfalls and Solutions

Forgetting AttributeUsage: Without the AttributeUsage attribute, your custom attribute defaults to AttributeTargets.All, allowing it anywhere. This causes confusion when developers apply it to inappropriate elements. Always explicitly declare valid targets.

Mutable attribute properties: Attribute instances are created through reflection and shared across multiple reads. If you make properties publicly settable, cached attribute instances might have modified values. Use read-only properties or constructor parameters for immutable data.

Expensive attribute constructors: Attribute constructors execute during reflection calls, not at compile time. Avoid database queries, file I/O, or complex computations in attribute constructors. Keep them lightweight with simple property assignments.

Ignoring inheritance behavior: Setting Inherited = true on AttributeUsage means derived classes automatically receive the attribute. This can cause unexpected behavior if the attribute carries class-specific data. Consider whether inheritance makes sense for your attribute's semantics.

Frequently Asked Questions (FAQ)

What is the purpose of custom attributes in C#?

Custom attributes let you add metadata to your code that can be read at runtime through reflection. They're essential for frameworks like ASP.NET, serialization libraries, validation systems, and testing frameworks. They enable declarative programming where you describe what you want rather than writing procedural code.

How do I restrict where my custom attribute can be used?

Use the AttributeUsage attribute on your custom attribute class. Set AttributeTargets to specify valid targets like Class, Method, or Property. Set AllowMultiple to true if the attribute can be applied multiple times to the same element. Set Inherited to control whether derived classes inherit the attribute.

Can custom attributes have parameters?

Yes, custom attributes can have positional parameters passed through constructors and named parameters via public properties or fields. Positional parameters are required and appear in a specific order. Named parameters are optional and can appear in any order. Parameter types are limited to simple types, Type, and arrays.

What are the performance implications of using attributes with reflection?

Reading attributes through reflection is relatively slow. Cache reflection results whenever possible, use GetCustomAttribute instead of GetCustomAttributes for single attributes, and consider source generators for compile-time alternatives. For frequently accessed attributes, store the reflection data in a static dictionary during application startup.

Back to Articles