Trim a .NET MAUI app

When it builds your app, .NET Multi-platform App UI (.NET MAUI) can use a linker called ILLink to reduce the overall size of the app with a technique known as trimming. ILLink reduces the size by analyzing the intermediate code produced by the compiler. It removes unused methods, properties, fields, events, structs, and classes to produce an app that contains only code and assembly dependencies that are necessary to run the app.

To prevent changes in behavior when trimming apps, .NET provides static analysis of trim compatibility through trim warnings. The trimmer produces trim warnings when it finds code that might not be compatible with trimming. If there are any trim warnings they should be fixed and the app should be thoroughly tested after trimming to ensure that there are no behavior changes. For more information, see Introduction to trim warnings.

Trimming behavior

Trimming behavior can be controlled by setting the $(TrimMode) build property to either partial or full:

<PropertyGroup>
  <TrimMode>full</TrimMode>
</PropertyGroup>

Important

The $(TrimMode) build property shouldn't be conditioned by build configuration. This is because features switches are enabled or disabled based on the value of the $(TrimMode) build property, and the same features should be enabled or disabled in all build configurations so that your code behaves identically.

The full trim mode removes any code that's not used by your app. The partial trim mode trims the base class library (BCL), assemblies for the underlying platforms (such as Mono.Android.dll and Microsoft.iOS.dll), and any other assemblies that have opted into trimming with the $(TrimmableAsssembly) build item:

<ItemGroup>
  <TrimmableAssembly Include="MyAssembly" />
</ItemGroup>

This is equivalent to setting [AssemblyMetadata("IsTrimmable", "True")] when building the assembly.

Note

It's not necessary to set the $(PublishTrimmed) build property to true in your app's project file, because this is set by default.

For more trimming options, see Trimming options.

Trimming defaults

By default, Android and Mac Catalyst builds use partial trimming when the build configuration is set to a release build. iOS uses partial trimming for any device builds, regardless of the build configuration, and doesn't use trimming for simulator builds.

Trimming incompatibilities

The following .NET MAUI features are incompatible with full trimming and will be removed by the trimmer:

Alternatively, you can use feature switches so that the trimmer preserves the code for these features. For more information, see Trimming feature switches.

For .NET trimming incompatibilities, see Known trimming incompatibilities.

Define a TypeConverter to replace an implicit conversion operator

It's not possible to rely on implicit conversion operators when assigning a value of an incompatible type to a property in XAML, or when two properties of different types use a data binding, when full trimming is enabled. This is because the implicit operator methods could be removed by the trimmer if they aren't used in your C# code. For more information about implicit conversion operators, see User-defined explicit and implicit conversion operators.

For example, consider the following type that defines implicit conversion operators between SizeRequest and Size:

namespace MyMauiApp;

public struct SizeRequest : IEquatable<SizeRequest>
{
    public Size Request { get; set; }
    public Size Minimum { get; set; }

    public SizeRequest(Size request, Size minimum)
    {
        Request = request;
        Minimum = minimum;
    }

    public SizeRequest(Size request)
    {
        Request = request;
        Minimum = request;
    }

    public override string ToString()
    {
        return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
    }

    public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);

    public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
    public static implicit operator Size(SizeRequest size) => size.Request;
    public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
    public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
    public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
    public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);
}

With full trimming enabled, the implicit conversion operators between SizeRequest and Size could be removed by the trimmer if they aren't used in your C# code.

Instead, you should define a TypeConverter for your type and attach it to the type using the TypeConverterAttribute:

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace MyMauiApp;

[TypeConverter(typeof(SizeRequestTypeConverter))]
public struct SizeRequest : IEquatable<SizeRequest>
{
    public Size Request { get; set; }
    public Size Minimum { get; set; }

    public SizeRequest(Size request, Size minimum)
    {
        Request = request;
        Minimum = minimum;
    }

    public SizeRequest(Size request)
    {
        Request = request;
        Minimum = request;
    }

    public override string ToString()
    {
        return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
    }

    public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);

    public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
    public static implicit operator Size(SizeRequest size) => size.Request;
    public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
    public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
    public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
    public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);

    private sealed class SizeRequestTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
            => sourceType == typeof(Size);

        public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
            => value switch
            {
                Size size => (SizeRequest)size,
                _ => throw new NotSupportedException()
            };

        public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
            => destinationType == typeof(Size);

        public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
        {
            if (value is SizeRequest sizeRequest)
            {
                if (destinationType == typeof(Size))
                    return (Size)sizeRequest;
            }
            throw new NotSupportedException();
        }
    }
}

Trimming feature switches

.NET MAUI has trimmer directives, known as feature switches, that make it possible to preserve the code for features that aren't trim safe. These trimmer directives can be used when the $(TrimMode) build property is set to full, as well as for NativeAOT:

MSBuild property Description
MauiEnableVisualAssemblyScanning When set to true, .NET MAUI will scan assemblies for types implementing IVisual and for [assembly:Visual(...)] attributes, and will register these types. By default, this build property is set to false.
MauiShellSearchResultsRendererDisplayMemberNameSupported When set to false, the value of SearchHandler.DisplayMemberName will be ignored. Instead, you should provide an ItemTemplate to define the appearance of SearchHandler results. By default, this build property is set to true.
MauiQueryPropertyAttributeSupport When set to false, [QueryProperty(...)] attributes won't be used to set property values when navigating. Instead, you should implement the IQueryAttributable interface to accept query parameters. By default, this build property is set to true.
MauiImplicitCastOperatorsUsageViaReflectionSupport When set to false, .NET MAUI won't look for implicit conversion operators when converting values from one type to another. This can affect bindings between properties with different types, and setting a property value of a bindable object with a value of a different type. Instead, you should define a TypeConverter for your type and attach it to the type using the TypeConverterAttribute attribute. By default, this build property is set to true.
_MauiBindingInterceptorsSupport When set to false, .NET MAUI won't intercept any calls to the SetBinding methods and won't try to compile them. By default, this build property is set to true.
MauiEnableXamlCBindingWithSourceCompilation When set to true, .NET MAUI will compile all bindings, including those where the Source property is used. If you enable this feature ensure that all bindings have the correct x:DataType so that they compile, or clear the data type with x:Data={x:Null}} if the binding shouldn't be compiled. By default, this build property is only set to true when full trimming or Native AOT deployment is enabled.

These MSBuild properties also have equivalent AppContext switches:

  • The MauiEnableVisualAssemblyScanning MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled.
  • The MauiShellSearchResultsRendererDisplayMemberNameSupported MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported.
  • The MauiQueryPropertyAttributeSupport MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported.
  • The MauiImplicitCastOperatorsUsageViaReflectionSupport MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported.
  • The _MauiBindingInterceptorsSupport MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported.
  • The MauiEnableXamlCBindingWithSourceCompilation MSBuild property has an equivalent AppContext switch named Microsoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabled.

The easiest way to consume a feature switch is by putting the corresponding MSBuild property into your app's project file (*.csproj), which causes the related code to be trimmed from the .NET MAUI assemblies.

Preserve code

When you use the trimmer, it sometimes removes code that you might have called dynamically, even indirectly. You can instruct the trimmer to preserve members by annotating them with the DynamicDependency attribute. This attribute can be used to express a dependency on either a type and subset of members, or at specific members.

Important

Every member in the BCL that can't be statically determined to be used by the app is subject to be removed.

The DynamicDependency attribute can be applied to constructors, fields, and methods:

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

In this example, the DynamicDependency ensures that the Helper method is kept. Without the attribute, trimming would remove Helper from MyAssembly or remove MyAssembly completely if it's not referenced elsewhere.

The attribute specifies the member to keep via a string or via the DynamicallyAccessedMembers attribute. The type and assembly are either implicit in the attribute context, or explicitly specified in the attribute (by Type, or by strings for the type and assembly name).

The type and member strings use a variation of the C# documentation comment ID string format, without the member prefix. The member string shouldn't include the name of the declaring type, and may omit parameters to keep all members of the specified name. The following examples show valid uses:

[DynamicDependency("Method()")]
[DynamicDependency("Method(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType", "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency("MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

Preserve assemblies

It's possible to specify assemblies that should be excluded from the trimming process, while allowing other assemblies to be trimmed. This approach can be useful when you can't easily use the DynamicDependency attribute, or don't control the code that's being trimmed.

When it trims all assemblies, you can tell the trimmer to skip an assembly by setting an TrimmerRootAssembly MSBuild item in the project file:

<ItemGroup>
  <TrimmerRootAssembly Include="MyAssembly" />
</ItemGroup>

Note

The .dll extension isn't required when setting the TrimmerRootAssembly MSBuild property.

If the trimmer skips an assembly, it's considered rooted, which means that it and all of its statically understood dependencies are kept. You can skip additional assemblies by adding more TrimmerRootAssembly MSBuild properties to the <ItemGroup>.

Preserve assemblies, types, and members

You can pass the trimmer an XML description file that specifies which assemblies, types, and members need to be retained.

To exclude a member from the trimming process when trimming all assemblies, set the TrimmerRootDescriptor MSBuild item in the project file to the XML file that defines the members to exclude:

<ItemGroup>
  <TrimmerRootDescriptor Include="MyRoots.xml" />
</ItemGroup>

The XML file then uses the trimmer descriptor format to define which members to exclude:

<linker>
  <assembly fullname="MyAssembly">
    <type fullname="MyAssembly.MyClass">
      <method name="DynamicallyAccessedMethod" />
    </type>
  </assembly>
</linker>

In this example, the XML file specifies a method that's dynamically accessed by the app, which is excluded from trimming.

When an assembly, type, or member is listed in the XML, the default action is preservation, which means that regardless of whether the trimmer thinks it's used or not, it's preserved in the output.

Note

The preservation tags are ambiguously inclusive. If you don’t provide the next level of detail, it will include all the children. If an assembly is listed without any types, then all the assembly’s types and members will be preserved.

Mark an assembly as trim safe

If you have a library in your project, or you're a developer of a reusable library and you want the trimmer to treat your assembly as trimmable, you can mark the assembly as trim safe by adding the IsTrimmable MSBuild property to the project file for the assembly:

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

This marks your assembly as "trimmable" and enables trim warnings for that project. Being "trimmable" means your assembly is considered compatible with trimming and should have no trim warnings when the assembly is built. When used in a trimmed app, the assembly's unused members are removed in the final output.

Setting the IsTrimmable MSBuild property to true in your project file inserts the AssemblyMetadata attribute into your assembly:

[assembly: AssemblyMetadata("IsTrimmable", "True")]

Alternatively, you can add the AssemblyMetadata attribute into your assembly without having added the IsTrimmable MSBuild property to the project file for your assembly.

Note

If the IsTrimmable MSBuild property is set for an assembly, this overrides the AssemblyMetadata("IsTrimmable", "True") attribute. This enables you to opt an assembly into trimming even if it doesn't have the attribute, or to disable trimming of an assembly that has the attribute.

Suppress analysis warnings

When the trimmer is enabled, it removes IL that's not statically reachable. Apps that use reflection or other patterns that create dynamic dependencies may be broken as a result. To warn about such patterns, when marking an assembly as trim safe, library authors should set the SuppressTrimAnalysisWarnings MSBuild property to false:

<PropertyGroup>
  <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>

Not suppressing trim analysis warnings will include warnings about the entire app, including your own code, library code, and SDK code.

Show detailed warnings

Trim analysis produces at most one warning for each assembly that comes from a PackageReference, indicating that the assembly's internals aren't compatible with trimming. As a library author, when you mark an assembly as trim safe, you should enable individual warnings for all assemblies by setting the TrimmerSingleWarn MSBuild property to false:

<PropertyGroup>
  <TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>

This setting shows all detailed warnings, instead of collapsing them to a single warning per assembly.

See also