Assembly Trimming: Remove Unused Code in C# Deployments

Assembly trimming is a .NET deployment optimization that analyzes your application, identifies unused code, and strips it away from your final published binaries. In self-contained C# deployments, where the entire .NET runtime and framework libraries are packaged alongside your application, file sizes can quickly balloon to 60MB or more. By enabling trimming, you can drastically reduce this footprint—often by 50% to 80%—resulting in faster downloads, reduced storage requirements, and quicker startup times.

What Is Assembly Trimming?

During the publish process, the .NET trimmer (an evolved version of the IL Linker) scans your application starting from the entry point (Main). It constructs a dependency graph by tracing all referenced types, methods, fields, and external assemblies. Any code or library that is not reached during this static analysis is considered dead code and is excluded from the final output directory.

Enabling Trimming in Self-Contained Deployments

To enable trimming, you must publish your application as a self-contained executable targeting a specific runtime identifier (RID).

1. Configuring the Project File (.csproj)

You can enable trimming permanently for release builds by adding the <PublishTrimmed> property inside a <PropertyGroup> in your .csproj file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

</Project>

2. Using the .NET CLI

Alternatively, you can enable trimming directly from the command line using the /p:PublishTrimmed=true flag with dotnet publish:

dotnet publish -c Release -r win-x64 --self-contained true /p:PublishTrimmed=true

Replace win-x64 with your target RID (such as linux-x64 or osx-arm64).

Trimming Granularity Levels

Modern .NET versions (NET 6 and later) support different trimming granularities via the <TrimMode> property:

  • Partial Trimming (<TrimMode>partial</TrimMode>): The default setting. It only trims assemblies that explicitly opt-in via the [assembly: AssemblyMetadata("IsTrimmable", "True")] attribute, which includes most of the core .NET runtime libraries. Application code and third-party libraries not marked as trimmable remain untouched.
  • Full Trimming (<TrimMode>full</TrimMode>): Aggressively analyzes and strips unused code at the member level across all assemblies, including your own application code and external NuGet packages. This yields the smallest possible binary size but requires thorough testing.

Handling Reflection and Dynamic Code

Because trimming relies on static code analysis, dynamic behaviors like reflection, late-bound dependency injection, and runtime serialization cannot always be detected. If the trimmer removes a method that is only called via reflection, your application will throw a MissingMethodException at runtime.

To prevent critical code from being trimmed:

  1. Annotate with Attributes: Use code annotations from the System.Diagnostics.CodeAnalysis namespace to inform the trimmer of dynamic dependencies.
    • [DynamicallyAccessedMembers]: Instructs the trimmer to preserve specific types, constructors, or methods.
    • [RequiresUnreferencedCode]: Marks a method as incompatible with trimming, generating build-time warnings when called.
  2. Exclude Specific Assemblies: If a third-party dependency is breaking due to trimming, you can prevent the trimmer from altering it by adding a <TrimmableAssembly> entry in your .csproj:
<ItemGroup>
  <TrimmableAssembly Remove="ProblematicLibrary" />
</ItemGroup>

Always test trimmed self-contained builds thoroughly in an environment that mirrors your target production deployment to verify that all runtime-loaded paths function correctly.