Source generators versus reflection

1 min read

Reflection reads an attribute at runtime and acts through calls such as GetValue. A Roslyn source generator reads it during compilation and writes real C# code that is compiled with the project and called directly.

Reflection

System.Text.Json in its default mode finds properties and attributes such as [JsonPropertyName] with reflection and reads values with GetValue. It reuses what it finds, but each read is still a dynamic call, slower than direct access.

C#
foreach (var prop in typeof(Person).GetProperties()){    object value = prop.GetValue(person); // reflection call    // write value to JSON, using attribute info found through reflection}

Source generator

For [JsonSerializable(typeof(Person))] on a partial class, the generator reads Person through Roslyn and writes a serializer into a .g.cs file. The class is partial because the generator supplies the other half. Calling JsonSerializer.Serialize(person, MyJsonContext.Default.Person) runs that generated code (simplified below).

C#
public static void WritePerson(Utf8JsonWriter writer, Person value){    writer.WriteStartObject();    writer.WriteNumber("user_id", value.UserId); // direct property access    writer.WriteEndObject();}

Native AOT

Native AOT compiles ahead of time with no JIT (see compilationThe CLI, the CLR and compilationWhat .NET is built on, and how C# source becomes running code.Opens in a new tab), and trimming removes code that looks unused, including members reflection would need. Reflection-based code can then fail at runtime. Generated code is compiled in, so nothing is trimmed or discovered later.

Where it matters

System.Text.Json source generation, minimal APIs under Native AOT and the Regex source generator use it. The ways attributes get read are listed in attributes and reflectionAttributes and reflectionAttributes tag code with metadata, and reflection reads that metadata back at runtime.Opens in a new tab.