Подготовка библиотек .NET для обрезки

Пакет SDK для .NET позволяет уменьшить размер автономных приложений путем обрезки. Обрезка удаляет неиспользуемый код из приложения и его зависимостей. Не все коды совместимы с обрезкой. .NET предоставляет предупреждения об обрезке для обнаружения шаблонов, которые могут нарушить обрезку приложений. В этой статье:

Необходимые компоненты

Пакет SDK для .NET 6 или более поздней версии.

Чтобы получить самые актуальные предупреждения обрезки и охват анализатора:

  • Установите и используйте пакет SDK для .NET 8 или более поздней версии.
  • Целевой net8.0 объект или более поздней версии.

Пакет SDK для .NET 7 или более поздней версии.

Чтобы получить самые актуальные предупреждения обрезки и охват анализатора:

  • Установите и используйте пакет SDK для .NET 8 или более поздней версии.
  • Целевой net8.0 объект или более поздней версии.

Пакет SDK для .NET 8 или более поздней версии.

Включение предупреждений об обрезке в библиотеке

Предупреждения обрезки в библиотеке можно найти с помощью любого из следующих методов:

  • Включение обрезки для конкретного проекта с помощью IsTrimmable свойства.
  • Создание тестового приложения для обрезки, использующего библиотеку и включение обрезки для тестового приложения. Не обязательно ссылаться на все API в библиотеке.

Мы рекомендуем использовать оба подхода. Обрезка для конкретного проекта удобна и отображает предупреждения об обрезки для одного проекта, но использует ссылки, помеченные как обрезные, чтобы просмотреть все предупреждения. Обрезка тестового приложения более трудоемка, но отображает все предупреждения.

Включение обрезки для конкретного проекта

Задайте <IsTrimmable>true</IsTrimmable> в файле проекта.

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

Задание свойства IsTrimmable MSBuild для true метки сборки как "обрезаемой" и включает предупреждения обрезки. "Trimmable" означает проект:

  • Считается совместимым с обрезкой.
  • При сборке не следует создавать предупреждения, связанные с обрезками. При использовании в обрезаном приложении сборка имеет неиспользуемые элементы, обрезанные в окончательных выходных данных.

Свойство IsTrimmable по умолчанию используется true при настройке проекта как совместимого с <IsAotCompatible>true</IsAotCompatible>AOT. Дополнительные сведения см . в анализаторах совместимости AOT.

Чтобы создать предупреждения об обрезки без маркировки проекта как совместимого с обрезкой, используйте <EnableTrimAnalyzer>true</EnableTrimAnalyzer> вместо <IsTrimmable>true</IsTrimmable>этого.

Отображение всех предупреждений с помощью тестового приложения

Чтобы отобразить все предупреждения анализа для библиотеки, триммер должен проанализировать реализацию библиотеки и всех зависимостей, которые использует библиотека.

При создании и публикации библиотеки:

  • Реализации зависимостей недоступны.
  • Доступные эталонные сборки не имеют достаточно сведений для триммера, чтобы определить, совместимы ли они с обрезкой.

Из-за ограничений зависимостей необходимо создать автономное тестовое приложение, которое использует библиотеку и его зависимости. Тестовое приложение содержит всю информацию, которую триммер требует, чтобы вывести предупреждение о несовместимости с обрезкой в:

  • Код библиотеки.
  • Код, ссылающийся на библиотеку из его зависимостей.

Примечание.

Если библиотека имеет другое поведение в зависимости от целевой платформы, создайте тестовое приложение обрезки для каждой из целевых платформ, поддерживающих обрезку. Например, если библиотека использует условную компиляцию , например #if NET7_0 для изменения поведения.

Чтобы создать тестовое приложение для обрезки, выполните следующие действия.

  • Создайте отдельный проект консольного приложения.
  • Добавьте ссылку на библиотеку.
  • Измените проект, аналогичный проекту, приведенному ниже, с помощью следующего списка:

Если библиотека нацелена на TFM, которая не является обрезаемой, например net472 или netstandard2.0нет преимуществ для создания тестового приложения обрезки. Обрезка поддерживается только для .NET 6 и более поздних версий.

  • Задайте для параметра <TrimmerDefaultAction> значение link.
  • Добавьте <PublishTrimmed>true</PublishTrimmed>.
  • Добавьте ссылку на проект библиотеки с <ProjectReference Include="/Path/To/YourLibrary.csproj" />помощью .
  • Укажите библиотеку в качестве корневой сборки триммера.<TrimmerRootAssembly Include="YourLibraryName" />
    • TrimmerRootAssembly обеспечивает анализ каждой части библиотеки. Он сообщает триммеру, что эта сборка является корневым. Сборка root означает, что триммер анализирует каждый вызов в библиотеке и проходит все пути кода, исходящие из этой сборки.
  • Добавьте <PublishTrimmed>true</PublishTrimmed>.
  • Добавьте ссылку на проект библиотеки с <ProjectReference Include="/Path/To/YourLibrary.csproj" />помощью .
  • Укажите библиотеку в качестве корневой сборки триммера.<TrimmerRootAssembly Include="YourLibraryName" />
    • TrimmerRootAssembly обеспечивает анализ каждой части библиотеки. Он сообщает триммеру, что эта сборка является корневым. Сборка root означает, что триммер анализирует каждый вызов в библиотеке и проходит все пути кода, исходящие из этой сборки.
  • Добавьте <PublishTrimmed>true</PublishTrimmed>.
  • Добавьте ссылку на проект библиотеки с <ProjectReference Include="/Path/To/YourLibrary.csproj" />помощью .
  • Укажите библиотеку в качестве корневой сборки триммера.<TrimmerRootAssembly Include="YourLibraryName" />
    • TrimmerRootAssembly обеспечивает анализ каждой части библиотеки. Он сообщает триммеру, что эта сборка является корневым. Сборка root означает, что триммер анализирует каждый вызов в библиотеке и проходит все пути кода, исходящие из этой сборки.

CSPROJ-файл

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
    <!-- Prevent warnings from unused code in dependencies -->
    <TrimmerDefaultAction>link</TrimmerDefaultAction>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="path/to/MyLibrary.csproj" />
    <!-- Analyze the whole library, even if attributed with "IsTrimmable" -->
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

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

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

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

Примечание. В предыдущем файле проекта при использовании .NET 7 замените <TargetFramework>net8.0</TargetFramework> на <TargetFramework>net7.0</TargetFramework>.

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

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

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

После обновления файла проекта запустите dotnet publish с помощью целевого идентификатора среды выполнения (RID).

dotnet publish -c Release -r <RID>

Следуйте приведенному выше шаблону для нескольких библиотек. Чтобы отображать предупреждения анализа обрезки одновременно для нескольких библиотек, добавьте их все в один проект в качестве элементов ProjectReference и TrimmerRootAssembly. Добавление всех библиотек в один и тот же проект с ProjectReferenceTrimmerRootAssembly элементами предупреждает о зависимостях, если какая-либо из корневых библиотек использует НЕуправляемый API в зависимости. Чтобы просмотреть предупреждения, связанные только с определенной библиотекой, следует ссылаться только на эту библиотеку.

Примечание. Результаты анализа зависят от сведений о реализации зависимостей. Обновление до новой версии зависимости может привести к предупреждениям анализа:

  • Если новая версия добавила неясные шаблоны отражения.
  • Даже если не было изменений в API.
  • Введение предупреждений об анализе обрезок является критическим изменением при использовании PublishTrimmedбиблиотеки.

Устранение предупреждений об обрезке

Описанные выше действия создают предупреждения о коде, которые могут вызвать проблемы при использовании в обрезаном приложении. В следующих примерах показаны наиболее распространенные предупреждения с рекомендациями по их устранению.

RequiresUnreferencedCode

Рассмотрим следующий код, который используется [RequiresUnreferencedCode] для указания того, что указанный метод требует динамического доступа к коду, который не ссылается статически, например через System.Reflection.

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

Предыдущий выделенный код указывает, что библиотека вызывает метод, явно помеченный как несовместимый с обрезкой. Чтобы избавиться от предупреждения, рассмотрите MyMethod необходимость вызова DynamicBehavior. Если да, заметите вызывающий объект MyMethod , с [RequiresUnreferencedCode] помощью которого распространяется предупреждение, чтобы вызывающие абоненты MyMethod получили предупреждение:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

После распространения атрибута вплоть до общедоступного API приложения, вызывающие библиотеку:

  • Получение предупреждений только для общедоступных методов, которые не являются обрезаемыми.
  • Не получайте предупреждения, как IL2104: Assembly 'MyLibrary' produced trim warnings.

DynamicallyAccessedMembers

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

В приведенном выше коде UseMethods вызывается метод отражения, имеющий [DynamicallyAccessedMembers] требование. Требование указывает, что общедоступные методы типа доступны. Удовлетворить требование путем добавления того же требования к параметру UseMethods.

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

Теперь все вызовы для UseMethods создания предупреждений, если они передают значения, которые не соответствуют требованию PublicMethods . [RequiresUnreferencedCode]Как и после распространения таких предупреждений на общедоступные API, все готово.

В следующем примере неизвестный тип передается в параметр аннотированного метода. Type Неизвестно из поля:

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

Аналогичным образом здесь проблема заключается в том, что поле type передается в параметр с этими требованиями. Исправлено путем добавления [DynamicallyAccessedMembers] в поле. [DynamicallyAccessedMembers] предупреждает о коде, который назначает несовместимые значения полю. Иногда этот процесс продолжается до тех пор, пока общедоступный API не будет аннотирован, и в другое время он заканчивается, когда конкретный тип переходит в расположение с этими требованиями. Например:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

В этом случае анализ обрезки сохраняет общедоступные методы Tupleи создает дополнительные предупреждения.

Рекомендации

  • Избегайте отражения, когда это возможно. При использовании отражения свести к минимуму область отражения, чтобы он был доступен только из небольшой части библиотеки.
  • Заметьте код со DynamicallyAccessedMembers статическим выражением требований к обрезке, когда это возможно.
  • Рекомендуется переорганизовать код, чтобы он соответствовал отанализируемому шаблону, который можно ознамещать с помощью DynamicallyAccessedMembers
  • Если код несовместим с обрезкой, аннотировать его и RequiresUnreferencedCode распространять эту заметку вызывающим абонентам до тех пор, пока соответствующие общедоступные API не будут аннотированы.
  • Избегайте использования кода, использующего отражение таким образом, чтобы не пониматься статическим анализом. Например, следует избегать отражения в статических конструкторах. Использование статически неанализируемого отражения в статических конструкторах приводит к тому, что предупреждение распространяется на все члены класса.
  • Избегайте аннотирования виртуальных методов или методов интерфейса. Для аннотирования виртуальных или интерфейсных методов требуется, чтобы все переопределения имели соответствующие заметки.
  • Если API в основном несовместим, альтернативные подходы к программированию к API могут быть рассмотрены. Распространенным примером являются сериализаторы на основе отражения. В таких случаях рассмотрите возможность внедрения других технологий, таких как генераторы источников, для создания кода, который проще анализировать статически. Например, см. раздел "Использование создания источника" в System.Text.Json

Устранение предупреждений для шаблонов, которые невозможно проанализировать

Лучше разрешать предупреждения, указывая назначение кода с помощью [RequiresUnreferencedCode] и DynamicallyAccessedMembers, когда это возможно. Однако в некоторых случаях может потребоваться включить обрезку библиотеки, использующую шаблоны, которые не могут быть выражены с этими атрибутами или без рефакторинга существующего кода. В этом разделе описаны некоторые расширенные способы устранения предупреждений анализа обрезки.

Предупреждение

Эти методы могут изменить поведение или код или привести к исключениям времени выполнения при неправильном использовании.

UnconditionalSuppressMessage

Рассмотрим код, который:

  • Намерение не может быть выражено с заметками.
  • Создает предупреждение, но не представляет реальную проблему во время выполнения.

Предупреждения можно отключить UnconditionalSuppressMessageAttribute. Это аналогично SuppressMessageAttribute, но сохраняется в IL и учитывается во время анализа обрезки.

Предупреждение

При подавлении предупреждений вы несете ответственность за гарантию совместимости с обрезкой кода на основе инвариантных, которые вы знаете, что это верно путем проверки и тестирования. Используйте осторожность с этими заметками, так как если они неверны или если инварианты изменения кода, они могут скрыть неправильный код.

Например:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

В приведенном выше коде свойство индексатора было аннотировано таким образом, чтобы возвращаемое Type соответствовало требованиям CreateInstance. Это гарантирует, что TypeWithConstructor конструктор хранится и что вызов CreateInstance не предупреждает. Заметка о наборе индексатора гарантирует, что все типы, хранящиеся в конструкторе Type[] . Однако анализ не может увидеть это и создает предупреждение для метода получения, так как он не знает, что возвращаемый тип имеет его конструктор.

Если вы уверены, что выполнены требования, вы можете замолчать это предупреждение, добавив [UnconditionalSuppressMessage] в метод получения:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

Важно подчеркнуть, что это допустимо только для подавления предупреждения, если есть заметки или код, которые гарантируют, что отраженные элементы являются видимыми целевыми объектами отражения. Недостаточно, чтобы член был объектом доступа к вызову, полю или свойству. Иногда этого достаточно, но в будущем такой код будет вызывать ошибки по мере добавления дополнительных оптимизаций по обрезке. Свойства, поля и методы, которые не являются видимыми целевыми объектами отражения, могут быть удалены, удалены их имена, перемещаются в разные типы или иначе оптимизированы таким образом, чтобы они отражались. При подавлении предупреждения можно выполнять отражение только тех целевых объектов, которые являются видимыми для анализатора обрезки в другом расположении.

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

DynamicDependency

Атрибут [DynamicDependency] можно использовать для указания того, что член имеет динамическую зависимость от других членов. В результате указанные элементы сохраняются при каждом сохранении элемента с атрибутом, но без вывода предупреждения. В отличие от других атрибутов, которые информируют анализ обрезки о поведении отражения кода, [DynamicDependency] сохраняет только другие элементы. Его можно использовать вместе с [UnconditionalSuppressMessage] для устранения некоторых предупреждений анализа.

Предупреждение

Используйте [DynamicDependency] атрибут только в качестве последнего способа, когда другие подходы недоступны. Предпочтительнее выразить поведение отражения с помощью [RequiresUnreferencedCode] или [DynamicallyAccessedMembers].

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

Без DynamicDependency обрезка может удалить Helper из MyAssembly или полностью удалить MyAssembly, если на него нет ссылок в других местах, что приведет к предупреждениям о возможном сбое во время выполнения. Атрибут гарантирует, что Helper будет сохранен.

Атрибут указывает элементы, которые должны быть сохранены, с помощью string или DynamicallyAccessedMemberTypes. Тип и сборка либо являются неявными в контексте атрибута, либо явно указаны в атрибуте (с помощью Type или string для типа и имени сборки).

В строках типа и элемента используется разновидность формата строки идентификатора комментария документации C# без префикса элемента. Строка-член не должна содержать имя декларационного типа и может опустить параметры, чтобы сохранить все члены указанного имени. Некоторые примеры формата показаны в следующем коде:

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(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<>))]

Атрибут [DynamicDependency] предназначен для использования в случаях, когда метод содержит шаблоны отражения, которые не могут быть проанализированы даже с помощью метода DynamicallyAccessedMembersAttribute.