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:
- Binding expressions where that binding path is set to a string. Instead, use compiled bindings. For more information, see Compiled bindings.
- 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. Instead, you should define a TypeConverter for your type and attach it to the type using the TypeConverterAttribute. For more information, see Define a TypeConverter to replace an implicit conversion operator.
- Loading XAML at runtime with the LoadFromXaml extension method. This XAML can be made trim safe by annotating all types that could be loaded at runtime with the
DynamicallyAccessedMembers
attribute or theDynamicDependency
attribute. However, this is very error prone and isn't recommended. - Receiving navigation data using the QueryPropertyAttribute. Instead, you should implement the IQueryAttributable interface on types that need to accept query parameters. For more information, see Process navigation data using a single method.
- The
SearchHandler.DisplayMemberName
property. Instead, you should provide an ItemTemplate to define the appearance of SearchHandler results. For more information, see Define search results item appearance.
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 namedMicrosoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled
. - The
MauiShellSearchResultsRendererDisplayMemberNameSupported
MSBuild property has an equivalent AppContext switch namedMicrosoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported
. - The
MauiQueryPropertyAttributeSupport
MSBuild property has an equivalent AppContext switch namedMicrosoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported
. - The
MauiImplicitCastOperatorsUsageViaReflectionSupport
MSBuild property has an equivalent AppContext switch namedMicrosoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported
. - The
_MauiBindingInterceptorsSupport
MSBuild property has an equivalent AppContext switch namedMicrosoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported
. - The
MauiEnableXamlCBindingWithSourceCompilation
MSBuild property has an equivalent AppContext switch namedMicrosoft.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 string
s 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.