Linking a .NET MAUI Mac Catalyst 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. 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.
Linker behavior
The linker supports three modes for .NET MAUI apps on iOS and Mac Catalyst:
- Don't link. Disabling linking ensures assemblies aren't modified.
- Link SDK assemblies only. In this mode, the linker leaves your assemblies untouched and reduces the size of the SDK assemblies by removing types and members that your app doesn't use.
- Link all assemblies. When it links all assemblies, the linker performs additional optimizations to make your app as small as possible. It modifies the intermediate code for your source code, which may break your app if you use features using an approach that the linker's static analysis can't detect. In these cases, you may need to make adjustments to your source code to make your app work correctly.
Linker behavior can be configured for each build configuration of your app.
Warning
Enabling the linker for your app's debug configuration may hinder your debugging experience, as it may remove property accessors that enable you to inspect the state of your objects.
To configure linker behavior in Visual Studio Code you should add the $(MtouchLink)
build property to a property group in your app's .csproj file. This build property should be set to None
, SdkOnly
, or Full
:
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<MtouchLink>SdkOnly</MtouchLink>
</PropertyGroup>
Alternatively, you can specify the linker behavior via the CLI when building and publishing your app. For more information, see Publish a .NET MAUI Mac Catalyst app.
Important
The $(MtouchLink)
build property can be set separately for each build configuration for your app.
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.
When using Native AOT deployment in .NET 9+, setting the IsAotCompatible
MSBuild property to true
also assigns a value of true
to the IsTrimmable
property, and enables additional AOT analyzer build properties. For more information about AOT analyzers, see AOT-compatibility analyzers. For more information about Native AOT deployment for .NET MAUI, see Native AOT deployment.
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.