Skip to content

perf: use reflection for attribute factories instead of source-generating attribute construction #5397

@thomhurst

Description

@thomhurst

Summary

Suggested by @teo-tsirpanis in dotnet/runtime#126541 (comment)

The source generator currently emits __Attributes(int groupIndex) methods that reconstruct attribute instances in code:

private static Attribute[] __Attributes(int groupIndex)
{
    switch (groupIndex)
    {
        case 0:
            return
            [
                new TestAttribute(),
                new CustomAttribute("Foo") { Property = "Bar" },
                // ... more attributes
            ];
    }
}

Since MethodInfo.GetCustomAttributes() is AOT-safe (Native AOT's reflection-free mode was deleted in dotnet/runtime#109857), we could replace the generated attribute factories with a reflection call:

// Instead of source-generating attribute construction, delegate to reflection
CreateAttributes = static (groupIndex) => methodInfo.GetCustomAttributes().ToArray(),

Benefits

  • Reduced generated code size — eliminates per-class __Attributes switch methods entirely
  • Fewer methods to JIT — one fewer generated method per test class
  • Always correct — no risk of source generator failing to replicate complex attribute constructors or property setters
  • Simpler generator code — removes PreGenerateAttributeFactoryBody and related generation logic

Current state

  • TUnit's __Attributes generates only method-level test-relevant attributes (not assembly metadata — the bloated example in the runtime issue was from a different project)
  • The reflection path (ReflectionAttributeExtractor.GetAllAttributes) already uses GetCustomAttributes() and caches results
  • Attribute factories are lazy — only invoked during test execution, not discovery — so the reflection cost is deferred

Considerations

  • Reflection has per-call overhead vs pre-built arrays, but attribute access is not a hot path (called once per test during setup)
  • Would need MethodInfo available at factory call time — currently the source-gen path avoids storing MethodInfo
  • The [DynamicallyAccessedMembers] annotations would need to cover attribute types
  • Could be behind a feature switch if perf characteristics differ between JIT and AOT

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions