Skip to content

Commit

Permalink
Trimming (#2574)
Browse files Browse the repository at this point in the history
* Limit linking docs to .NET 8

* Trimming on Android.

* Add feature switches

* Edit.

* Edits.

* Fix bookmark.

* Edits.

* Edit title.

* Edit.

* Edits.

* Edits.

* Fix link.

* Move file to correct location.

* Edit.

* Edit.

* Edit.

* Edit.

* Edits.

* Edits.

* Edits.

* Edit.

* Edits.

* Add note.

* Move content to include files.

* Correct filename.

* Move headings out of includes.

* Edits.

* Edit.

* Surround example with `<PropertyGroup>`

Hope it's ok, I just added this for clarity.

* Final edits.

---------

Co-authored-by: Jonathan Peppers <[email protected]>
  • Loading branch information
davidbritch and jonathanpeppers authored Oct 24, 2024
1 parent 3351cb8 commit 7fbdd49
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 52 deletions.
2 changes: 2 additions & 0 deletions docs/TOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,8 @@
href: windows/deployment/publish-unpackaged-cli.md
- name: Publish with Visual Studio to a folder
href: windows/deployment/publish-visual-studio-folder.md
- name: Trimming
href: deployment/trimming.md
- name: Unit testing
href: deployment/unit-testing.md
- name: Enterprise application patterns
Expand Down
1 change: 1 addition & 0 deletions docs/android/linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: "Linking a .NET MAUI Android app"
description: "Learn about the .NET for Android linker, which is used to eliminate unused code from a .NET MAUI Android app in order to reduce its size."
ms.date: 08/27/2024
no-loc: [ ILLink ]
monikerRange: "=net-maui-8.0"
---

# Linking a .NET MAUI Android app
Expand Down
25 changes: 25 additions & 0 deletions docs/deployment/includes/feature-switches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
ms.topic: include
ms.date: 10/22/2024
monikerRange: ">=net-maui-9.0"
---

.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 <xref:Microsoft.Maui.Controls.ItemsView.ItemTemplate> to define the appearance of <xref:Microsoft.Maui.Controls.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 <xref:Microsoft.Maui.Controls.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 <xref:System.ComponentModel.TypeConverter> for your type and attach it to the type using the <xref:System.ComponentModel.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`. |

These MSBuild properties also have equivalent <xref:System.AppContext> switches:

- The `MauiEnableVisualAssemblyScanning` MSBuild property has an equivalent <xref:System.AppContext> switch named `Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled`.
- The `MauiShellSearchResultsRendererDisplayMemberNameSupported` MSBuild property has an equivalent <xref:System.AppContext> switch named `Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported`.
- The `MauiQueryPropertyAttributeSupport` MSBuild property has an equivalent <xref:System.AppContext> switch named `Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported`.
- The `MauiImplicitCastOperatorsUsageViaReflectionSupport` MSBuild property has an equivalent <xref:System.AppContext> switch named `Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported`.
- The `_MauiBindingInterceptorsSupport` MSBuild property has an equivalent <xref:System.AppContext> switch named `Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported`.

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.
13 changes: 13 additions & 0 deletions docs/deployment/includes/trimming-incompatibilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
ms.topic: include
ms.date: 10/23/2024
monikerRange: ">=net-maui-9.0"
---

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](~/fundamentals/data-binding/compiled-bindings.md).
- 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 <xref:System.ComponentModel.TypeConverter> for your type and attach it to the type using the <xref:System.ComponentModel.TypeConverterAttribute>. For more information, see [Define a TypeConverter to replace an implicit conversion operator](~/deployment/trimming.md#define-a-typeconverter-to-replace-an-implicit-conversion-operator).
- Loading XAML at runtime with the <xref:Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml%2A> extension method. This XAML can be made trim safe by annotating all types that could be loaded at runtime with the [`DynamicallyAccessedMembers`](xref:System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute) attribute or the [`DynamicDependency`](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute) attribute. However, this is very error prone and isn't recommended.
- Receiving navigation data using the <xref:Microsoft.Maui.Controls.QueryPropertyAttribute>. Instead, you should implement the <xref:Microsoft.Maui.Controls.IQueryAttributable> interface on types that need to accept query parameters. For more information, see [Process navigation data using a single method](~/fundamentals/shell/navigation.md#process-navigation-data-using-a-single-method).
- The `SearchHandler.DisplayMemberName` property. Instead, you should provide an <xref:Microsoft.Maui.Controls.ItemsView.ItemTemplate> to define the appearance of <xref:Microsoft.Maui.Controls.SearchHandler> results. For more information, see [Define search results item appearance](~/fundamentals/shell/search.md#define-search-results-item-appearance).
4 changes: 3 additions & 1 deletion docs/deployment/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Deployment & testing"
description: "Learn how to deploy, test, and publish .NET MAUI apps to Android, iOS, macOS, and Windows."
ms.date: 08/27/2024
ms.date: 10/21/2024
---

# Deployment & testing
Expand All @@ -12,6 +12,8 @@ Unit testing checks that each unit of functionality in your app performs as expe

There are many techniques for increasing the performance, and perceived performance, of .NET MAUI apps. Collectively these techniques can greatly reduce the amount of work being performed by a CPU, and the amount of memory consumed by an app. For more information, see [Improve app performance](performance.md).

When it builds your app, .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. For more information, see [Trim a .NET MAUI app](trimming.md).

## Android

You can debug and test your apps on the Android emulator, which can be run in a variety of configurations to simulate different devices. Each configuration is called a *virtual device*. When you deploy and test your apps on the emulator, you select a pre-configured or custom virtual device that simulates a physical Android device such as a Pixel phone. For more information, see [Debug on the Android Emulator](~/android/emulator/debug-on-emulator.md).
Expand Down
176 changes: 176 additions & 0 deletions docs/deployment/trimming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
title: "Trim a .NET MAUI app"
description: "Learn about the .NET trimmer, which eliminates unused code from a .NET MAUI app to reduce its size."
ms.date: 10/24/2024
no-loc: [ ILLink ]
monikerRange: ">=net-maui-9.0"
---

# 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](/dotnet/core/deploying/trimming/fixing-warnings).

## Trimming behavior

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

```xml
<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:

```xml
<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](/dotnet/core/deploying/trimming/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

[!INCLUDE [Trimming incompatibilities](includes/trimming-incompatibilities.md)]

Alternatively, you can use feature switches so that the trimmer preserves the code for these features. For more information, see [Trimming feature switches](#trimming-feature-switches).

For .NET trimming incompatibilities, see [Known trimming incompatibilities](/dotnet/core/deploying/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](/dotnet/csharp/language-reference/operators/user-defined-conversion-operators).

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

```csharp
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 <xref:System.ComponentModel.TypeConverter> for your type and attach it to the type using the <xref:System.ComponentModel.TypeConverterAttribute>:

```csharp
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

[!INCLUDE [Trimming feature switches](includes/feature-switches.md)]

[!INCLUDE [Control the trimmer](../includes/linker-control.md)]

## See also

- [Trim self-contained deployments and executables](/dotnet/core/deploying/trimming/trim-self-contained)
19 changes: 0 additions & 19 deletions docs/includes/feature-switches.md

This file was deleted.

Loading

0 comments on commit 7fbdd49

Please sign in to comment.