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:
- Create a class that inherits from
System.Attribute
- Name it with the "Attribute" suffix (convention)
- 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:
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:
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:
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:
[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:
// 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:
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:
// 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:
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:
[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:
-
Constructor parameters must be compile-time constants:
- Primitive types (
int
,string
,bool
, etc.) Type
objects- Arrays of the above
- Enum values
- Primitive types (
-
Named parameters can only be:
- Public fields
- Public properties with getters and setters
This code would cause a compilation error:
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:
- Create attribute classes by inheriting from
System.Attribute
- Control attribute usage with
[AttributeUsage]
- Apply attributes to code elements
- 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
- Create a
[Deprecated]
attribute that marks methods as deprecated with a reason and suggested alternative. - Build a simple dependency injection framework that uses
[Inject]
attributes to identify which properties need to be injected. - Create a logging framework that uses attributes to specify logging levels for different classes and methods.
- Implement a simple REST API framework where endpoints are defined using attributes like
[Route("/users")]
.
Additional Resources
- Microsoft Docs: Custom Attributes
- C# Attributes Best Practices
- Framework Design Guidelines: Designing Extensible Components
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! :)