Attributes, reflection and source generators

C# · Advanced · 14 min

Serializers, test frameworks, ASP.NET Core and ORMs are driven by attributes and reflection or source generators.

Attributes attach metadata to code: [Obsolete("use V2")], [JsonPropertyName("id")], [Fact], [HttpGet("/items")]. You define one by deriving from Attribute, with [AttributeUsage] restricting where it can be applied. Attributes do nothing alone; some other code reads them.

Reflection (System.Reflection) inspects and uses types at runtime:

  • typeof(T) or obj.GetType() gives a Type.
  • GetProperties(), GetMethods(), GetCustomAttributes() enumerate members and metadata.
  • PropertyInfo.GetValue(obj), MethodInfo.Invoke(obj, args), Activator.CreateInstance(type) act on them.

Reflection is flexible but slow compared to direct calls, hides dependencies from the compiler, and breaks with trimming and Native AOT, because the trimmer can't see which members you'll access by name. Cache PropertyInfo objects, or compile expressions/delegates once if you must reflect in a hot path.

Source generators are the modern alternative: compiler plugins (Roslyn incremental generators) that inspect your code at build time and add new C# source. System.Text.Json uses [JsonSerializable] contexts, Regex uses [GeneratedRegex], and logging uses [LoggerMessage], all producing fast, AOT-friendly code with no runtime reflection. Partial classes and partial methods (and C# 13 partial properties) are the hooks generators fill in.

Example

using System.Reflection;
using System.Text.RegularExpressions;

foreach (var prop in typeof(User).GetProperties())
{
    var attr = prop.GetCustomAttribute<ColumnAttribute>();
    Console.WriteLine($"{prop.Name} -> {attr?.Name ?? prop.Name.ToLower()}");
}
// Id -> user_id / Email -> email

var u = new User(7, "a@b.c");
Console.WriteLine(typeof(User).GetProperty("Email")!.GetValue(u)); // a@b.c
Console.WriteLine(Patterns.Digits().IsMatch("abc123"));            // True

[AttributeUsage(AttributeTargets.Property)]
class ColumnAttribute(string name) : Attribute { public string Name => name; }

record User([property: Column("user_id")] int Id, string Email);

static partial class Patterns
{
    [GeneratedRegex(@"\d+")]            // source-generated at compile time
    public static partial Regex Digits();
}

Remember

  • Attributes are metadata; frameworks read them
  • Reflection inspects and invokes members at runtime but is slow and trim-unfriendly
  • Source generators produce code at build time instead
  • Prefer generator-based APIs (JSON contexts, GeneratedRegex) for AOT

Watch out for

  • Calling GetProperties/GetCustomAttribute repeatedly in a hot path instead of caching the result.

Check yourself

  1. Why do source generators work with Native AOT when reflection-heavy code often doesn't?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/