C# Attributes and Reflection

Define C# attributes, inspect public metadata with reflection, distinguish discovery from execution, and account for trimming and AOT deployment limits.

Lesson 36 of 43Advanced10 min read

A test runner recognizes a test method because metadata marks it for discovery. A serializer can read an attribute to choose a JSON property name. An attribute does not perform those actions by itself. It supplies information that a framework or your own code chooses to interpret. Reflection provides a way to inspect available types and members at runtime and retrieve that metadata.

This lesson uses .NET 8 and C# 12 in an ordinary console application. You will define a small display-label attribute, inspect a known type's public properties, and build a worked summary printer. The examples keep discovery narrow and explicit. Later deployment modes such as trimming and Native AOT require additional planning because not every dynamically discovered member is guaranteed to remain available.

Attributes declare information rather than run a feature

A custom attribute derives from System.Attribute. Its constructor and public properties describe the metadata it can carry. AttributeUsage limits valid targets and controls whether multiple copies or inherited metadata are permitted. These rules make the contract clearer to both the compiler and a consumer.

Attribute arguments must use the forms supported for attribute metadata, so an attribute is not a container for arbitrary runtime objects or secret configuration. Applying a display label does not cause a property to print itself. A consumer must look for the label and decide how to use it. Keep attribute responsibilities small: a marker or configuration hint is easier to understand than a constructor that performs network work or depends on mutable global state.

Reflection inspects runtime type information

typeof(T) obtains metadata for a type known at compile time. An object's GetType() reports its actual runtime type. Reflection APIs then expose properties, methods, constructors and other members under the requested binding rules. Restricting discovery to public instance properties avoids accidentally including static members or private implementation details.

Reflection introduces runtime failure possibilities that ordinary member access resolves at compile time. A property may not exist, may be an indexer, or may have an inaccessible getter. Invoking a getter can execute application code and throw. Metadata discovery and execution should therefore be considered separately. A diagnostic tool that inspects unknown objects must not assume reading every property is side-effect-free or safe for sensitive data.

Retrieve metadata with explicit fallback behavior

GetCustomAttribute<T> can return null when the requested attribute is absent. Define a fallback rather than relying on every property being decorated. A label reader can use the property's name as a default. If multiple attributes are allowed, use the appropriate collection-oriented API and define their order or combination rule.

Reflection's member enumeration order should not become an accidental user-interface contract. Sort properties explicitly when deterministic output matters. The main example uses ordinal property-name sorting, making its output stable and easy to test. The same principle applies to plugins or commands: discovery should produce a predictable set under deliberate filters, and registration should reject conflicting identifiers rather than silently choosing whichever member appears first.

Dynamic invocation has a cost and a trust boundary

Discovering a method by name and invoking it bypasses some compile-time guarantees. Arguments can be incompatible, a member can be absent, and exceptions may be wrapped by reflection invocation APIs. Do not hide all such failures behind a generic success response. Validate the discovered shape and preserve useful bounded diagnostics.

Do not load arbitrary assemblies or invoke arbitrary members merely because a user supplied a name. Plugin systems need trusted sources, explicit interfaces and a registration policy. A whitelist of known types is easier to reason about than broad assembly scanning. When the type is already known, direct code is often clearer and faster. Reflection is most useful when metadata-driven flexibility is a real requirement rather than an excuse to avoid a small explicit mapping.

Trimming and AOT change deployment assumptions

A trimmer removes code that appears unused, while dynamic discovery can be difficult for static analysis to predict. Reflection that works in a development build may therefore need annotations, preserved member requirements or a different design in a trimmed deployment. Native AOT also limits certain runtime code-generation patterns. Read and resolve the relevant analysis warnings rather than assuming successful compilation proves every discovery path works.

Source-generated alternatives can move known metadata work to compile time and improve deployment predictability. A serializer's supported source-generation mode is one example. Test the actual publish mode used by the application, especially when a library scans for members dynamically. Keep deployment constraints in the library's contract and avoid claiming that every arbitrary reflection operation is automatically compatible with trimming or AOT.

Official references

C# Attributes and Reflection Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Inspect a display label

This complete program reads public instance property metadata from a known type. It retrieves the label without constructing a Customer or invoking its getters.

The undecorated property uses its own name. The attribute's metadata becomes meaningful only because this program interprets it. No sensitive customer values are read during this metadata-only operation.

C#
using System;
using System.Linq;
using System.Reflection;

foreach (PropertyInfo property in typeof(Customer)
    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
    .OrderBy(p => p.Name, StringComparer.Ordinal))
{
    string label = property.GetCustomAttribute<DisplayLabelAttribute>()
        ?.Text ?? property.Name;
    Console.WriteLine($"{property.Name}: {label}");
}

[AttributeUsage(AttributeTargets.Property,
    AllowMultiple = false, Inherited = false)]
public sealed class DisplayLabelAttribute : Attribute
{
    public string Text { get; }
    public DisplayLabelAttribute(string text) => Text = text;
}

public sealed class Customer
{
    [DisplayLabel("Customer name")]
    public string Name { get; init; } = "";

    public int Number { get; init; }
}
Output
Name: Customer name
Number: Number

C# Attributes and Reflection in Real Projects

Worked exercise: print a known object's safe summary

The task is to print values from a known summary type. Include only public readable non-indexed instance properties, sort their names, and use the display-label fallback. This is a complete worked implementation. It is intentionally restricted to a known non-sensitive type whose getters are simple, rather than advertised as a universal object dumper.

C#
using System;
using System.Linq;
using System.Reflection;

var summary = new OrderSummary { Count = 3, Status = "Ready" };
foreach (PropertyInfo property in typeof(OrderSummary)
    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
    .Where(p => p.GetMethod?.IsPublic == true &&
                p.GetIndexParameters().Length == 0)
    .OrderBy(p => p.Name, StringComparer.Ordinal))
{
    string label = property.GetCustomAttribute<LabelAttribute>()
        ?.Text ?? property.Name;
    Console.WriteLine($"{label}: {property.GetValue(summary)}");
}

[AttributeUsage(AttributeTargets.Property)]
public sealed class LabelAttribute : Attribute
{
    public string Text { get; }
    public LabelAttribute(string text) => Text = text;
}

public sealed class OrderSummary
{
    [Label("Items")]
    public int Count { get; init; }
    public string Status { get; init; } = "";
}
Output
Items: 3
Status: Ready

Filtering avoids indexers that require arguments, but a getter could still throw in a different type. Keep the known-type precondition or design an explicit error and redaction policy before generalizing the helper.

C# Attributes and Reflection FAQ

Does applying an attribute execute its intended behavior?

No. Applying it records metadata under the attribute contract. A framework or your own consumer must retrieve and interpret it. Attribute instances may be constructed during retrieval, so constructors should stay predictable and lightweight. A test marker does not run a test without a runner, and a display label does not create a user interface without a reader.

Is reflection access equivalent to direct property access?

Both can invoke a getter, but reflection adds runtime discovery and compatibility checks. Direct access lets the compiler verify the member and its type. Reflection can fail because a requested member is missing or unsuitable, and a getter can still throw. Prefer direct access for known fixed operations, and validate reflected member shape when metadata-driven behavior is necessary.

Why filter out indexed properties?

An indexer requires one or more arguments to read a value. Calling GetValue without them is not the same operation as reading an ordinary property and can fail. A summary printer should either exclude indexers or supply a deliberate indexing contract. Filtering public readable non-indexed instance properties narrows the operation, but does not guarantee that every remaining getter is harmless or nonsensitive.

How can I make reflection work in trimmed applications?

Use supported annotations or explicit preserved-member contracts, resolve analysis warnings, and test the trimmed publish output. When the set of types is known, an explicit registry or source-generated code may be simpler. There is no blanket annotation that makes arbitrary future plugins safe under every deployment mode. Document the exact discovery needs and verify them under the application's target runtime.

C# Attributes and Reflection Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A reflection-heavy library is published with trimming. What should be checked?

C# Attributes and Reflection Quiz

2 questions to check what you learned.

  1. What is the role of a custom attribute?

    • Automatically execute a method when the program starts.
    • Make every property getter side-effect-free.
    • Attach declarative metadata that a consumer can interpret.
    • Give the class access to another type's private fields.
  2. A reflection summary printer wants deterministic simple-property output. Which approach fits?

    • Rely on unspecified member enumeration order.
    • Read private and indexed properties without checks.
    • Invoke every method before retrieving attributes.
    • Filter public readable non-indexed instance properties and sort explicitly.