0Pricing
C# Academy · Lesson

Trimming & Reflection Limitations

Handle trim warnings, use [DynamicallyAccessedMembers], and replace runtime reflection with source-generated alternatives.

Trimming & Reflection Limitations is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is IL Trimming?

IL Trimming is a publish-time optimization that removes unused types, methods, and assemblies from the output. The result is a smaller executable. It is automatically enabled for Native AOT and self-contained single-file publishes.

Enabling Trimming

Enable trimming in the project file. You can control the trim mode from partial (trim unused assemblies only) to full (trim unused members within assemblies).

// .csproj
<PropertyGroup>
  <!-- Enable trimming on publish -->
  <PublishTrimmed>true</PublishTrimmed>

  <!-- full: aggressive — trims unused members within assemblies
       partial: conservative — only removes unused assemblies -->
  <TrimMode>full</TrimMode>

  <!-- Treat all trimmer warnings as errors (recommended for CI) -->
  <TrimmerRootAssemblies>MyApp</TrimmerRootAssemblies>
  <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>

// Publish:
dotnet publish -c Release -r linux-x64 --self-contained

How the Trimmer Works

The trimmer performs a reachability analysis starting from entry points (Main, registered services, attributes). Any code not reachable from those roots is removed.

// Example: only MyApp.Program.Main is a root
// Code flow:
//   Main → WebApplication.Run → MapGet → MyHandler
//   Everything else is trimmed

// What gets KEPT:
class MyHandler { public string Handle() => "ok"; }

// What gets TRIMMED (if not reached):
class UnusedService { public void DoWork() { } }

// Problem: reflection can access UnusedService at runtime
// But the trimmer can't know that at compile time
// → UnusedService is trimmed → MissingMethodException at runtime

Reflection and Trimming

Reflection is the primary source of trimming incompatibilities. The trimmer can't statically analyze which types you'll access via Type.GetType() or Activator.CreateInstance().

// PROBLEMATIC: type name comes from config at runtime
var typeName = config["Plugin:Type"]!;
var type = Type.GetType(typeName);      // type may have been trimmed!
var instance = Activator.CreateInstance(type!); // MissingMethodException

// PROBLEMATIC: LINQ expressions with reflection
var query = dbContext.Set<T>()          // T discovered via reflection
    .Where(BuildExpression<T>("Name", "Alice"));

// PROBLEMATIC: attribute scanning
var handlers = Assembly.GetExecutingAssembly()
    .GetTypes()
    .Where(t => t.HasCustomAttribute<HandlerAttribute>());
// All handler types may be trimmed away

DynamicDependency Attribute

[DynamicDependency] tells the trimmer to preserve specific members that you know will be accessed via reflection.

// Keep all public constructors of MyPlugin:
[DynamicDependency(
    DynamicallyAccessedMemberTypes.PublicConstructors,
    typeof(MyPlugin))]
public static IPlugin CreatePlugin()
    => (IPlugin)Activator.CreateInstance(typeof(MyPlugin))!;

// Keep a specific method by name:
[DynamicDependency("ProcessOrder", typeof(OrderHandler))]
public static void Bootstrap() { }

// Keep everything on a type (use sparingly):
[DynamicDependency(
    DynamicallyAccessedMemberTypes.All,
    typeof(LegacyReflectionHelper))]
public static void EnsurePreserved() { }

DynamicallyAccessedMembers Annotation

[DynamicallyAccessedMembers] annotates parameters, fields, and properties to tell the trimmer what members of a Type will be accessed reflectively at runtime.

// Annotate a method parameter:
public void Register(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
    Type serviceType)
{
    // trimmer knows to preserve public constructors of whatever Type is passed
    var instance = Activator.CreateInstance(serviceType)!;
}

// Annotate a generic parameter:
public static T Create<
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    T>() where T : new()
    => new T();

// Annotate a field:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
private Type _modelType = typeof(OrderModel);

RequiresUnreferencedCode

Mark methods that fundamentally rely on reflection with [RequiresUnreferencedCode]. This surfaceswarnings to callers so they know the code may fail when trimmed.

// Mark a method that uses reflection internally:
[RequiresUnreferencedCode("Uses Type.GetType() — not safe for trimming")]
public static object LoadPlugin(string typeName)
{
    var type = Type.GetType(typeName)!
        ?? throw new InvalidOperationException($"Type {typeName} not found");
    return Activator.CreateInstance(type)!;
}

// Callers will see a trimmer warning:
// IL2026: Members decorated with [RequiresUnreferencedCode]
// need their callers to be decorated with the same attribute
// or be suppressed with [UnconditionalSuppressMessage]

// The warning reminds callers to use a trim-safe alternative
// or add a DynamicDependency for the types they expect

Trim-Safe Alternatives

Many reflection-heavy patterns have trim-safe alternatives in modern .NET. Prefer these to avoid trimming issues.

// INSTEAD OF: Type.GetType(name) + Activator.CreateInstance
// USE: registered factories or switch expressions:
public IHandler CreateHandler(string name) => name switch
{
    "order" => new OrderHandler(),
    "email" => new EmailHandler(),
    _       => throw new ArgumentException($"Unknown: {name}")
};

// INSTEAD OF: reflection-based JSON serialization
// USE: source-generated JsonSerializerContext

// INSTEAD OF: attribute scanning via Assembly.GetTypes()
// USE: compile-time source generators

// INSTEAD OF: dynamic proxy libraries (Castle, DispatchProxy)
// USE: source-generated interceptors (.NET 8+)

Trim Descriptor XML

Use an XML root descriptor to preserve entire namespaces or assemblies that the trimmer cannot analyze — for example, third-party reflection-heavy libraries.

<!-- TrimmerRoots.xml -->
<linker>
  <!-- Keep everything in LegacyLib -->
  <assembly fullname="LegacyLib">
    <type fullname="LegacyLib.*" preserve="all" />
  </assembly>

  <!-- Keep a specific type -->
  <assembly fullname="MyApp">
    <type fullname="MyApp.Plugins.PluginLoader" preserve="all" />
  </assembly>
</linker>

// Reference in .csproj:
<ItemGroup>
  <TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>

Real-World: Fixing Trimming Warnings

A practical workflow for resolving trimmer warnings in a real codebase.

// Step 1: enable analyzer during development
// .csproj:
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>

// Step 2: build and read warnings:
// warning IL2026: 'MyMapper.MapDynamic(Type)' uses
// [RequiresUnreferencedCode] via 'Type.GetMethod'

// Step 3: fix the root cause:
// BEFORE:
public void MapDynamic(Type t)
{
    var method = t.GetMethod("MapFrom")!; // IL2026
    method.Invoke(null, null);
}

// AFTER: use an interface instead of reflection
public void Map(IMapper mapper) => mapper.MapFrom();

// Step 4: if fix is not feasible, add annotation and document:
[RequiresUnreferencedCode("Uses reflection: ensure target types are preserved")]
public void MapDynamic(Type t) { /* ... */ }

Quick Check

What does the [DynamicallyAccessedMembers] attribute tell the IL trimmer?

Recap: Trimming & Reflection Limitations

Key takeaways:

  • Trimming removes unreachable code at publish time — reflection breaks this analysis
  • Enable with <PublishTrimmed>true</PublishTrimmed>; use TrimMode=full for maximum savings
  • [DynamicDependency]: explicitly preserve specific members for reflection
  • [DynamicallyAccessedMembers]: annotate Type parameters so the trimmer knows what to preserve
  • [RequiresUnreferencedCode]: warn callers that a method is trim-unsafe
  • Prefer source generators and interfaces over reflection-heavy patterns

Frequently asked questions

Is the “Trimming & Reflection Limitations” lesson free?

Yes — the full text of “Trimming & Reflection Limitations” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “Trimming & Reflection Limitations”?

Handle trim warnings, use [DynamicallyAccessedMembers], and replace runtime reflection with source-generated alternatives. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Trimming & Reflection Limitations” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Native AOT Compilation
  2. Trimming & Reflection Limitations
  3. ReadyToRun & Tiered Compilation
  4. Benchmarking with BenchmarkDotNet
← Back to C# Academy