Skip to main content

C# Custom Attributes

Attributes in C# are a powerful way to add metadata to your code. While the .NET framework provides many built-in attributes like [Obsolete], [Serializable], or [Required], creating your own custom attributes opens up new possibilities for enhancing your code with additional information that can be accessed at runtime.

What Are Custom Attributes?

Custom attributes are user-defined annotations that you can attach to various program elements such as classes, methods, properties, and more. These annotations don't directly affect the behavior of your code but provide a way to store additional information that can be retrieved using reflection.

Think of attributes as sticky notes attached to your code elements that provide extra context or instructions.

Why Use Custom Attributes?

Custom attributes are useful in many scenarios:

  • Configuration: Mark classes or properties that require special treatment
  • Code generation: Provide hints to tools that generate code
  • Serialization: Control how objects are serialized
  • Validation: Define validation rules for properties
  • Documentation: Enhance documentation with additional metadata
  • Framework development: Create extensible frameworks that inspect and act on attributes

Creating Custom Attributes

Let's dive into creating custom attributes step by step:

Step 1: Define a Custom Attribute Class

To create a custom attribute, you need to:

  1. Create a class that inherits from System.Attribute
  2. Name it with the "Attribute" suffix (convention)
  3. Define what kind of data it should store

Here's a simple example of a custom attribute that marks methods with an author name and date:

csharp
using System;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Date { get; }

public AuthorAttribute(string name, string date)
{
Name = name;
Date = date;
}
}

Let's break down this code:

  • We inherit from System.Attribute
  • We use the [AttributeUsage] attribute to specify that our attribute can only be applied to methods and cannot be used multiple times on the same element
  • We define two properties to store the author's name and date
  • We create a constructor to initialize these properties

Step 2: Apply Your Custom Attribute

Once defined, you can apply your custom attribute to code elements. When applying the attribute, you can omit the "Attribute" suffix:

csharp
public class Calculator
{
[Author("John Doe", "2023-09-15")]
public int Add(int a, int b)
{
return a + b;
}

[Author("Jane Smith", "2023-09-20")]
public int Subtract(int a, int b)
{
return a - b;
}
}

Step 3: Access Custom Attributes with Reflection

After applying custom attributes, you can use reflection to read them at runtime:

csharp
using System;
using System.Reflection;

public class Program
{
public static void Main()
{
// Get the Calculator type
Type calculatorType = typeof(Calculator);

// Get the methods from the Calculator class
MethodInfo[] methods = calculatorType.GetMethods();

// Loop through each method
foreach (MethodInfo method in methods)
{
// Try to get the AuthorAttribute if it exists
AuthorAttribute authorAttribute = method.GetCustomAttribute<AuthorAttribute>();

// If the attribute exists, display its information
if (authorAttribute != null)
{
Console.WriteLine($"Method: {method.Name}");
Console.WriteLine($"Author: {authorAttribute.Name}");
Console.WriteLine($"Date: {authorAttribute.Date}");
Console.WriteLine();
}
}
}
}

Output:

Method: Add
Author: John Doe
Date: 2023-09-15

Method: Subtract
Author: Jane Smith
Date: 2023-09-20

Customizing Attribute Usage

The [AttributeUsage] attribute lets you control how your custom attribute can be used:

csharp
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method, // Can be applied to classes and methods
AllowMultiple = true, // Can be applied multiple times to the same element
Inherited = false)] // Won't be inherited by derived classes
public class MyCustomAttribute : Attribute
{
// Attribute implementation
}

The AttributeTargets enum provides options like:

  • Class, Method, Property, Field
  • Parameter, ReturnValue
  • Assembly, Module
  • All (for all possible targets)

Real-World Examples

Let's explore some practical applications of custom attributes:

Example 1: Validation Attributes

Custom attributes can define validation rules for properties:

csharp
// Define the attribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class StringLengthAttribute : Attribute
{
public int MinLength { get; }
public int MaxLength { get; }

public StringLengthAttribute(int minLength, int maxLength)
{
MinLength = minLength;
MaxLength = maxLength;
}

public bool IsValid(string value)
{
if (value == null)
return MinLength == 0;

return value.Length >= MinLength && value.Length <= MaxLength;
}
}

// Use the attribute
public class User
{
[StringLength(5, 20)]
public string Username { get; set; }

[StringLength(8, 30)]
public string Password { get; set; }
}

// Validator that uses reflection
public class ModelValidator
{
public List<string> Validate<T>(T model)
{
List<string> errors = new List<string>();
Type type = typeof(T);

foreach (PropertyInfo property in type.GetProperties())
{
StringLengthAttribute lengthAttr = property.GetCustomAttribute<StringLengthAttribute>();
if (lengthAttr != null)
{
string value = property.GetValue(model) as string;
if (!lengthAttr.IsValid(value))
{
errors.Add($"{property.Name} must be between {lengthAttr.MinLength} and {lengthAttr.MaxLength} characters.");
}
}
}

return errors;
}
}

Usage example:

csharp
User user = new User { Username = "Joe", Password = "pass" };
ModelValidator validator = new ModelValidator();
List<string> validationErrors = validator.Validate(user);

foreach (string error in validationErrors)
{
Console.WriteLine(error);
}

Output:

Username must be between 5 and 20 characters.
Password must be between 8 and 30 characters.

Example 2: Configuration Attributes

Attributes can configure how a serializer works with your classes:

csharp
// Define attributes
[AttributeUsage(AttributeTargets.Class)]
public class SerializableEntityAttribute : Attribute
{
public string TableName { get; }

public SerializableEntityAttribute(string tableName)
{
TableName = tableName;
}
}

[AttributeUsage(AttributeTargets.Property)]
public class SerializablePropertyAttribute : Attribute
{
public string ColumnName { get; }
public bool IsPrimaryKey { get; }

public SerializablePropertyAttribute(string columnName, bool isPrimaryKey = false)
{
ColumnName = columnName;
IsPrimaryKey = isPrimaryKey;
}
}

// Use attributes
[SerializableEntity("Products")]
public class Product
{
[SerializableProperty("ProductID", isPrimaryKey: true)]
public int Id { get; set; }

[SerializableProperty("ProductName")]
public string Name { get; set; }

[SerializableProperty("UnitPrice")]
public decimal Price { get; set; }

// This property won't be serialized (no attribute)
public string InternalNotes { get; set; }
}

// Simple ORM that reads the configuration from attributes
public class SimpleORM
{
public string GenerateCreateTableSQL<T>()
{
Type type = typeof(T);

SerializableEntityAttribute entityAttr = type.GetCustomAttribute<SerializableEntityAttribute>();
if (entityAttr == null)
throw new ArgumentException($"Type {type.Name} is not marked with SerializableEntityAttribute");

string tableName = entityAttr.TableName;

List<string> columnDefinitions = new List<string>();
PropertyInfo[] properties = type.GetProperties();

foreach (PropertyInfo prop in properties)
{
SerializablePropertyAttribute propAttr = prop.GetCustomAttribute<SerializablePropertyAttribute>();
if (propAttr != null)
{
string columnDef = $"{propAttr.ColumnName} {GetSqlType(prop.PropertyType)}";
if (propAttr.IsPrimaryKey)
columnDef += " PRIMARY KEY";

columnDefinitions.Add(columnDef);
}
}

return $"CREATE TABLE {tableName} (\n {string.Join(",\n ", columnDefinitions)}\n);";
}

private string GetSqlType(Type type)
{
if (type == typeof(int))
return "INTEGER";
else if (type == typeof(string))
return "TEXT";
else if (type == typeof(decimal))
return "DECIMAL(18,2)";
else
return "TEXT";
}
}

Usage example:

csharp
SimpleORM orm = new SimpleORM();
string sql = orm.GenerateCreateTableSQL<Product>();
Console.WriteLine(sql);

Output:

CREATE TABLE Products (
ProductID INTEGER PRIMARY KEY,
ProductName TEXT,
UnitPrice DECIMAL(18,2)
);

Attribute Constructors and Named Parameters

Custom attributes support both constructor parameters (positional) and named parameters:

csharp
[AttributeUsage(AttributeTargets.Class)]
public class InfoAttribute : Attribute
{
// Positional parameters (must be provided when using the attribute)
public string Version { get; }

// Optional named parameters (can be set when using the attribute)
public string Developer { get; set; }
public bool IsStable { get; set; }

public InfoAttribute(string version)
{
Version = version;
IsStable = true; // Default value
}
}

// Using the attribute with both positional and named parameters
[Info("1.0.3", Developer = "DevTeam", IsStable = false)]
public class MyLibrary
{
// Class implementation
}

Attribute Restrictions

There are some limitations on what you can include in attributes:

  1. Constructor parameters must be compile-time constants:

    • Primitive types (int, string, bool, etc.)
    • Type objects
    • Arrays of the above
    • Enum values
  2. Named parameters can only be:

    • Public fields
    • Public properties with getters and setters

This code would cause a compilation error:

csharp
public class InvalidAttribute : Attribute
{
// Error: Parameter type must be a constant type
public InvalidAttribute(DateTime createdAt)
{
// DateTime is not allowed as a constructor parameter
}
}

Summary

Custom attributes provide a powerful way to enhance your C# code with metadata:

  1. Create attribute classes by inheriting from System.Attribute
  2. Control attribute usage with [AttributeUsage]
  3. Apply attributes to code elements
  4. Use reflection to read attributes at runtime

Custom attributes are ideal for scenarios where you need to:

  • Add metadata to your code
  • Create extensible frameworks
  • Build tools that analyze code
  • Configure how objects are processed

By mastering custom attributes, you can create more flexible and configurable applications that can adapt to changing requirements.

Exercises

  1. Create a [Deprecated] attribute that marks methods as deprecated with a reason and suggested alternative.
  2. Build a simple dependency injection framework that uses [Inject] attributes to identify which properties need to be injected.
  3. Create a logging framework that uses attributes to specify logging levels for different classes and methods.
  4. Implement a simple REST API framework where endpoints are defined using attributes like [Route("/users")].

Additional Resources

Happy coding with custom attributes!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)