Attributes and reflection

3 min read

Attributes

Attributes are declarative tags that add metadata to code elements such as classes, methods, properties, assemblies, etc. This metadata doesn’t directly affect execution flow, but it can be queried at runtime using reflection to influence behavior.

Reflection

Reflection provides the ability for a program to inspect its own metadata (types, members, assemblies) at runtime. It allows for dynamic creation of type instances, invocation of methods, and accessing fields and properties, even if their names are not known at compile time.

In code

Line 3 defines a custom attribute, AuthorAttribute. The commented code then uses reflection to read it: from the class on lines 19 and 20, and from the method on lines 26 and 27.

C#
// Define a custom attribute public class AuthorAttribute : Attribute{    public string Name { get; }    public double Version { get; set; } = 1.0;     public AuthorAttribute(string name) { Name = name; }}  public class MyService{     public void ProcessData() { /*... */ }} // Using Reflection to read the attribute// Type serviceType = typeof(MyService);// AuthorAttribute classAuthor = (AuthorAttribute)Attribute.GetCustomAttribute(serviceType, typeof(AuthorAttribute));// if (classAuthor!= null)// {//     Console.WriteLine($"Class Author: {classAuthor.Name}, Version: {classAuthor.Version}");// } // MethodInfo methodInfo = serviceType.GetMethod("ProcessData");// AuthorAttribute methodAuthor = (AuthorAttribute)Attribute.GetCustomAttribute(methodInfo, typeof(AuthorAttribute));// if (methodAuthor!= null)// {//     Console.WriteLine($"Method Author: {methodAuthor.Name}");// }

The Microsoft Learn example in the sources also demonstrates using reflection to dynamically create and invoke a genericGenericsType parameters as placeholders, so one piece of code works for many types and stays type-safe.Opens in a new tab method.

Attributes are just classes

An attribute is a class that inherits from Attribute. By convention its name ends in Attribute, and you drop that suffix when you use it, so AuthorAttribute is used as [Author("Alex")]. On its own it does nothing. It’s a label on the code, until something reads it back.

Reflection in a few lines

Reflection is C#’s ability to look at types, methods, properties and attributes while the program is running. In the example, you’re not calling .Name or .Age directly. You are asking the type itself which properties it has, and finding out at runtime.

C#
Person p = new Person();Type t = p.GetType(); Console.WriteLine(t.Name);      // "Person"Console.WriteLine(t.Namespace); // wherever Person lives foreach (var prop in t.GetProperties())    Console.WriteLine(prop.Name); // lists every public property, by name, at runtime

How frameworks use them

Attributes like [Route("api/users")], [Required], [JsonPropertyName("user_id")] and [TestMethod] do nothing by themselves. A separate piece of code, such as the web framework, the validator, the serializer or the test runner, uses reflection to scan your assembly, find them, and change its behavior. This is a simplified version of what a test runner does. It has never heard of your test class, but it can still find and run the tests.

C#
Type testClass = typeof(MyTests); foreach (var method in testClass.GetMethods()){    if (method.GetCustomAttribute<TestMethodAttribute>() != null)    {        Console.WriteLine($"Running {method.Name}...");        method.Invoke(Activator.CreateInstance(testClass), null); // calls it dynamically    }}

That is the trick behind test frameworks, DI containers, ORMs and serializers. Reflection is slower than direct code, so it’s typically used for setup, configuration and startup logic, not inside tight loops. And it isn’t the only way an attribute gets read, which is the next section.

Four ways attributes get read

Reflection is one mechanism, but not the only one. These are the four, from the most dynamic to the least:

How When the attribute is read Used by
Runtime reflection Every time, while the app runs Simple or older validation logic, custom reflection code
Reflection, cached once Once at startup, then reused ASP.NET Core routing, EF Core model building
Source generators At compile time, which generates real code System.Text.Json source generation, minimal API AOT, the Regex source generator
Compiler and analyzers At compile time, with no runtime role [Obsolete], nullable annotations, Roslyn analyzers

The second row is what routingRoutingHow an incoming URL finds the code that handles it.Opens in a new tab and the EF Core modelORMs, DbContext and entitiesWhat EF Core is, and the four ideas its API is built on.Opens in a new tab do. The third is the direction modern .NET is moving, and it has its own note: source generators versus reflectionSource generators versus reflectionA source generator does at build time what reflection does at runtime.Opens in a new tab. The fourth is how the compiler uses the nullable attributesNullable reference typesReference types are non-nullable by default, and the compiler warns when that might not hold.Opens in a new tab.

Where it matters

Attributes are widely used in .NET for serialization (for example [XmlElement]), validation in ASP.NET Core, test metadata (for example [Fact]) and ORM mappings (for example [Column]). Reflection is a powerful tool for building frameworks (like DI containers, ORMs and testing frameworks), serialization libraries and plug-in architectures. However, reflection can be slower than direct code execution and bypasses some compile-time checks, so it should be used judiciously, especially in performance-critical code paths. That cost is why frameworks cache what they find, and why source generators exist.


Sources