CodeGym /Courses /C# SELF /Source Generators an...

Source Generators and System.Reflection.Emit

C# SELF
Level 63, Lesson 4
Available

1. Introduction

Let's be honest: most people think about code generation roughly the same way they think about an egg slicer — useful, but rarely vital. However modern .NET projects are getting more complex, and automating routine or boilerplate code isn't just a "nice-to-have" — it's an important tool to improve quality and productivity.

Source Generators are a mechanism introduced starting with C# 9 and .NET 5. They act like compile-time superheroes: they can generate C# code that then gets compiled as part of your project. They don't touch already compiled assembly code at runtime — they only extend the project with new source files before compilation.

Below are typical use cases for Source Generators.

Automatic generation of boilerplate code

In many projects you need to write repetitive code: constructors, ToString methods, serializers, property change notification (INotifyPropertyChanged), etc. Source Generators can generate that code automatically, saving developers from boring work and typos.

Example: ToString generator

Suppose you create DTO classes (Data Transfer Object) for passing data. You need a meaningful ToString for each class that lists all properties.

Instead of copying by hand you can create a Source Generator that for each class annotated with [AutoToString] will generate the ToString implementation. Libraries like AutoToString do this.


// Your class with the attribute
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator will generate (simplified):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Pros: readable code that is always up-to-date when you add new properties.

Simplifying serialization and deserialization

Source Generators are used inside the standard library System.Text.Json to generate fast serialization and deserialization code. Before generators, serialization often relied on reflection (expensive), whereas generated code is fast and safe.

What does the developer get? You annotate [JsonSerializable(typeof(MyType))] — and the generator creates high-performance serialization code for that type.

Code generation for config, mappers, DI containers

  • Configs: generators can auto-create configuration classes based on JSON files.
  • Mappers: e.g. the Mapster project uses generators to create mapping between types without manual field copying.
  • Dependency Injection: some containers (for example, StrongInject) use generators to produce service registration code.

Integration with external infrastructure

Some generators analyze external resources (API descriptions, GraphQL schemas, Thrift protocols, etc.) and generate C# classes to work with them. That avoids manual code updates when contracts change.

Compile-time checks and diagnostics

Source Generators can be used for code checks: "If a project contains method X but condition Y is not met — emit a compiler warning!". Many linters and analyzers work like this, and a generator can add messages or even inject a ready-made stub.

2. Your own Source Generator for automatic serialization

Let's look at a small straight-forward example: a generator that for a class annotated with [AutoJson] generates a method to serialize to JSON.

The code below is illustrative only; in real life use System.Text.Json.SourceGeneration.


// Manually written:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Generator will add:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Interaction diagram between Source Generator and the project

flowchart TD
    A(Your Source Code) -->|Compiler calls| B(Source Generator)
    B -->|Added Code| C(New .cs files)
    C --> D(Project compilation)
  • First the compiler (Roslyn) analyzes your sources.
  • Then it calls "your" generator (implementing ISourceGenerator).
  • The generator adds new .cs files that become part of the compilation tree.
  • As a result the assembly contains both your code and the generated code.

Specialized tasks: what else you can generate

  • Bindings for native libraries (wrappers over C code or the WinAPI).
  • AOP: auto-logging of calls, aspects (in the spirit of Fody, PostSharp, etc.).
  • API descriptions for documentation generators.
  • Processing external resources: SVG, SQL, Razor — the generator creates strongly-typed classes for type-safe access.

3. Dynamic code generation with System.Reflection.Emit

If Source Generators generate C# code before the build, System.Reflection.Emit is a tool for magic at runtime. Your program can create new types, methods and even assemblies itself — on the fly!

Sounds scary? A bit. But sometimes there's no alternative: for example, if you're building frameworks for dynamic proxying (AOP, profiling, mocking), runtime serializers based on runtime data, dynamic ORMs, etc.

When you need Reflection.Emit

  • Types are not known beforehand (the user defines structure on the fly).
  • Dynamic proxying (wrappers to intercept calls).
  • High-performance serialization (for example, protobuf-net).
  • Plugin and scripting engines with complex loading scenarios.

What you can create with Reflection.Emit

  • AssemblyBuilder — create a new assembly.
  • ModuleBuilder — a module inside an assembly.
  • TypeBuilder — description of a new type.
  • MethodBuilder — a method with IL code.
  • PropertyBuilder, FieldBuilder, EventBuilder — properties, fields, events.

Mini example: building a new class on the fly

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Create assembly and module
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Create a new class
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Add a public string property Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Done! Create the Type
        return typeBuilder.CreateTypeInfo();
    }
}

You can now use this type like a regular C# object, for example via reflection:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# in faces");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# in faces

This is how ORMs, proxies, serializers, profilers and even some testing frameworks are born.

4. Useful nuances

Source Generators vs Reflection.Emit: who beats whom?

Source Generators work at compile time: they make your source code smarter, and the result gets into the compiled assembly. You cannot use them to generate code based on data obtained while the program is already running.

Reflection.Emit works at runtime: it lets you create assemblies, types and methods dynamically, but such code is harder to debug and maintain.

When to use what?

A real-life analogy:

  • Source Generators are like a factory that produces ready parts before the car is assembled.
  • Reflection.Emit is like an engineer who welds a rocket engine onto the car while it's driving.

Gotchas and caveats

  • Generated code can be hard to debug. Many generators can save sources to disk — look for them in the obj\Generated folder.
  • Reflection.Emit creates assemblies in memory that are not unloaded from the AppDomain. Use AssemblyBuilderAccess.RunAndCollect for temporary assemblies (if supported).
  • Don't overuse Reflection.Emit for simple tasks — sometimes a source generator or a plain code template is simpler and more reliable.
  • Generators require understanding Roslyn (for Source Generators) and IL (for Reflection.Emit).

Source Generators and Reflection.Emit

Criterion Source Generators Reflection.Emit
When used At compile time At runtime
Result C# sources, part of the assembly IL code, new types/assemblies
Common scenarios Auto-generation of boilerplate, DI, mapping, serialization Proxies, dynamic ORMs, special runtime pipelines
Difficulty Medium, requires Roslyn knowledge High, requires IL knowledge
IDE support & debugging Great (sources visible) Hard
Performance Very high Can be high

5. Practical scenarios

1. Generating strongly-typed API

An organization provides an OpenAPI specification. A generator parses it and creates controller classes, DTOs and client code to work with the REST API — type-safe and with IntelliSense support.

Code (pseudo):
// Spec: GET /users -> returns User[]
// Source Generator will generate:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Automatic injection/DI container (Compile-time IoC)

Generators create builders for registering dependencies and building the object graph. You don't have to write services.AddSingleton<IMyService, MyService>() manually.

Code (pseudo):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator will generate:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Dynamic proxies — example using Reflection.Emit

The Castle DynamicProxy library builds proxy types that intercept method calls — the basis for AOP, logging, tracing, mocking.

Code (simplified):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // Call is intercepted, you can log/change the result

4. Fast serialization without reflection

Instead of building type descriptions at runtime via reflection (slow), Source Generators can generate serialization/deserialization code in advance — as fast as possible with no overhead.

2
Task
C# SELF, level 63, lesson 4
Locked
Dynamic Class Creation Using Reflection.Emit
Dynamic Class Creation Using Reflection.Emit
1
Survey/quiz
Reflection, level 63, lesson 4
Unavailable
Reflection
Reflection and dynamic types
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION