Custom attributes let you add declarative metadata to types, methods, properties, and parameters. The attribute is stored in the assembly’s metadata and read at runtime with reflection. ASP.NET Core is saturated with attributes: [ApiController], [HttpGet], [Route], [Authorize], [Required], [MaxLength] — all implemented with exactly the same attribute mechanism available to your own code. Building custom attributes for validation, documentation, auditing, and routing is a powerful way to make your code declarative and reduce repetition.
Defining a Custom Attribute
// ── Define a custom attribute ──────────────────────────────────────────────
// Attributes must inherit from System.Attribute
[AttributeUsage(
AttributeTargets.Property | AttributeTargets.Field, // where it can be applied
AllowMultiple = false, // can appear at most once per target
Inherited = true)] // derived classes inherit this attribute
public sealed class SlugAttribute : Attribute
{
// Positional parameter (required) — set via constructor
public int MaxLength { get; }
// Named parameter (optional) — set via property initialiser
public bool AutoGenerate { get; set; } = true;
public SlugAttribute(int maxLength)
{
if (maxLength <= 0)
throw new ArgumentOutOfRangeException(nameof(maxLength), "MaxLength must be positive.");
MaxLength = maxLength;
}
}
// ── Apply the attribute ────────────────────────────────────────────────────
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
[Slug(50, AutoGenerate = true)] // positional then named
public string Slug { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
typeof() expressions, enum values, and arrays of these. You cannot use new DateTime(2025, 1, 1) or a method call as an attribute argument. This is also why you cannot use null for a non-nullable constructor parameter in an attribute without careful handling.sealed. Attribute inheritance is rarely useful and can cause subtle issues where a derived attribute is applied but only the base attribute’s contract is checked. Making attributes sealed prevents accidental inheritance and makes the attribute’s behaviour predictable. The .NET framework attributes like ObsoleteAttribute and RequiredAttribute are all sealed for this reason.PropertyInfo to its validation attributes, then use that dictionary for all subsequent validation calls. This is exactly how ASP.NET Core’s DataAnnotationsValidator works — it builds a metadata cache on first validation of a given type.Reading Attributes at Runtime
// ── Read a specific attribute from a property ─────────────────────────────
PropertyInfo? slugProp = typeof(Post).GetProperty(nameof(Post.Slug));
SlugAttribute? slugAttr = slugProp?.GetCustomAttribute<SlugAttribute>();
if (slugAttr is not null)
{
Console.WriteLine($"MaxLength: {slugAttr.MaxLength}"); // 50
Console.WriteLine($"AutoGenerate: {slugAttr.AutoGenerate}"); // true
}
// ── Build a validation framework using attributes ─────────────────────────
public static class AttributeValidator
{
public static Dictionary<string, string[]> Validate(object model)
{
var errors = new Dictionary<string, string[]>();
var type = model.GetType();
foreach (var prop in type.GetProperties())
{
var value = prop.GetValue(model);
// Check [Required] attribute
var required = prop.GetCustomAttribute<RequiredAttribute>();
if (required is not null && value is null or "")
errors[prop.Name] = [$"{prop.Name} is required."];
// Check [MaxLength] attribute
var maxLen = prop.GetCustomAttribute<MaxLengthAttribute>();
if (maxLen is not null && value is string str && str.Length > maxLen.Length)
errors[prop.Name] = [$"{prop.Name} max length is {maxLen.Length}."];
// Check custom [Slug] attribute
var slug = prop.GetCustomAttribute<SlugAttribute>();
if (slug is not null && value is string slugVal)
{
if (slugVal.Length > slug.MaxLength)
errors[prop.Name] = [$"Slug exceeds max length of {slug.MaxLength}."];
if (slugVal.Contains(' '))
errors[prop.Name] = ["Slug cannot contain spaces."];
}
}
return errors;
}
}
Common Mistakes
Mistake 1 — Non-constant attribute argument (compile error)
❌ Wrong — new DateTime(…) is not a compile-time constant:
[ValidFrom(new DateTime(2025, 1, 1))] // compile error!
✅ Correct — use string representation or separate integer arguments:
[ValidFrom("2025-01-01")] // parse the string inside the attribute
Mistake 2 — AttributeTargets mismatch (compile error at usage)
❌ Wrong — applying a property attribute to a class:
[Slug(50)] // on a class — compile error if AttributeTargets.Property only!
✅ Correct — verify the AttributeUsage is set to the correct target(s).