Préparer des bibliothèques .NET pour suppression

Le Kit de développement logiciel (SDK) .NET permet de réduire la taille des applications autonomes en les découpant. Le découpage supprime le code inutilisé de l’application et de ses dépendances. Certains codes ne sont pas compatibles avec le découpage. .NET fournit des avertissements d’analyse de découpage pour détecter les modèles qui peuvent interrompre les applications découpées. Cet article :

Prerequisites

SDK .NET 6 ou version ultérieure.

Pour obtenir les avertissements de découpage les plus à jour et la couverture de l’analyseur :

  • Installez et utilisez le Kit de développement logiciel (SDK) .NET 8 ou version ultérieure.
  • Ciblez net8.0 ou une version ultérieure.

SDK .NET 7 ou version ultérieure.

Pour obtenir les avertissements de découpage les plus à jour et la couverture de l’analyseur :

  • Installez et utilisez le Kit de développement logiciel (SDK) .NET 8 ou version ultérieure.
  • Ciblez net8.0 ou une version ultérieure.

SDK .NET 8 ou version ultérieure.

Activer les avertissements de suppression dans la bibliothèque

Les avertissements de découpage dans une bibliothèque sont disponibles avec l’une des méthodes suivantes :

  • L’activation de la suppression spécifique au projet à l’aide de la propriété IsTrimmable.
  • La création d’une application de test de découpage qui utilise la bibliothèque et l’activation du découpage pour l’application de test. Il n’est pas nécessaire de référencer toutes les API de la bibliothèque.

Nous vous recommandons d’utiliser les deux approches. Le découpage spécifique au projet est pratique et affiche des avertissements de découpage pour un projet, mais s’appuie sur le fait que les références sont marquées compatibles avec le découpage pour afficher tous les avertissements. La suppression d’une application demande plus de travail, mais affiche tous les avertissements.

Activer la suppression propre au projet

Définissez <IsTrimmable>true</IsTrimmable> dans le fichier projet.

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

La définition de la propriété MSBuild de IsTrimmable à true marque l’assembly comme « trimmable » et active les avertissements de découpage. « Trimmable » signifie que le projet :

  • Est considéré comme compatible avec le découpage.
  • Ne doit pas générer d’avertissements liés au découpage lors de la génération. Lorsqu’il est utilisé dans une application à laquelle des découpages ont été appliqués, l’assembly voit ses membres inutilisés découpés dans la sortie finale.

La propriété IsTrimmable utilise la valeur par défaut true lors de la configuration d’un projet étant compatible avec AOA avec <IsAotCompatible>true</IsAotCompatible>. Pour plus d’informations, consultez la section Analyseur de compatibilité AOT.

Pour générer des avertissements de découpage sans marquer le projet comme compatible avec le découpage, utilisez <EnableTrimAnalyzer>true</EnableTrimAnalyzer> plutôt que <IsTrimmable>true</IsTrimmable>.

Afficher tous les avertissements avec l’application de test

Pour afficher tous les avertissements d’analyse d’une bibliothèque, le découpage doit analyser l’implémentation de la bibliothèque et de toutes les dépendances que la bibliothèque utilise.

Lors de la génération et de la publication d’une bibliothèque :

  • Les implémentations des dépendances ne sont pas disponibles.
  • Les assemblys de référence disponibles n’ont pas suffisamment d’informations pour le découpage pour déterminer s’ils sont compatibles avec le découpage.

En raison des limitations de dépendances, une application de test autonome qui utilise la bibliothèque et ses dépendances doit être créée. L’application de test inclut toutes les informations dont le découpage a besoin pour émettre un avertissement sur les incompatibilités de découpage dans les éléments suivants :

  • Le code de la bibliothèque.
  • Le code référencé par la bibliothèque à partir de ses dépendances.

Remarque

Si la bibliothèque a un comportement différent en fonction des frameworks cible, créez une application de test de découpage pour chacune des frameworks cible qui prennent en charge le découpage. Par exemple, si la bibliothèque utilise la compilation conditionnelle, telle que #if NET7_0 pour modifier le comportement.

Pour créer l’application de test de découpage :

  • Créez un projet d’application console distinct.
  • Ajoutez une référence à la bibliothèque.
  • Modifiez le projet similaire au projet illustré ci-dessous à l’aide de la liste suivante :

Si la bibliothèque cible un TFM qui n’est pas possible d’être découpé, par exemple net472 ou netstandard2.0, il n’existe aucun avantage de créer une application de test de découpage. Le découpage n’est pris en charge que pour .NET 6 et versions ultérieures.

  • Définissez <TrimmerDefaultAction> sur link.
  • Ajoutez <PublishTrimmed>true</PublishTrimmed>.
  • Ajoutez une référence au projet de bibliothèque avec <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Spécifiez la bibliothèque en tant qu’assembly racine de découpage avec <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantit que chaque partie de la bibliothèque est analysée. Il indique au découpage que cet assembly est une « racine ». Un assembly « racine » signifie que le découpage analyse chaque appel de la bibliothèque et traverse tous les chemins de code provenant de cet assembly.
  • Ajoutez <PublishTrimmed>true</PublishTrimmed>.
  • Ajoutez une référence au projet de bibliothèque avec <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Spécifiez la bibliothèque en tant qu’assembly racine de découpage avec <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantit que chaque partie de la bibliothèque est analysée. Il indique au découpage que cet assembly est une « racine ». Un assembly « racine » signifie que le découpage analyse chaque appel de la bibliothèque et traverse tous les chemins de code provenant de cet assembly.
  • Ajoutez <PublishTrimmed>true</PublishTrimmed>.
  • Ajoutez une référence au projet de bibliothèque avec <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Spécifiez la bibliothèque en tant qu’assembly racine de découpage avec <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantit que chaque partie de la bibliothèque est analysée. Il indique au découpage que cet assembly est une « racine ». Un assembly « racine » signifie que le découpage analyse chaque appel de la bibliothèque et traverse tous les chemins de code provenant de cet assembly.

Fichier .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>

Remarque : dans le fichier projet précédent, lors de l’utilisation de .NET 7, remplacez <TargetFramework>net8.0</TargetFramework> par <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>

Une fois le fichier projet mis à jour, exécutez dotnet publish avec l’identificateur d’exécution cible (RID).

dotnet publish -c Release -r <RID>

Suivez le modèle précédent pour plusieurs bibliothèques. Pour afficher les avertissements d’analyse de suppression pour plusieurs bibliothèques à la fois, ajoutez-les toutes au même projet en tant qu’éléments ProjectReference et TrimmerRootAssembly. Le fait d’ajouter toutes les bibliothèques au même projet avec ProjectReference et aux éléments TrimmerRootAssembly avertit les dépendances si l’unedes bibliothèques racines utilise une API n’étant pas totalement compatible avec le découpage dans une dépendance. Pour voir les avertissements qui sont liés uniquement à une bibliothèque particulière, référencez cette bibliothèque uniquement.

Remarque : les résultats de l’analyse dépendent des détails d’implémentation de vos dépendances. La mise à jour vers une nouvelle version d’une dépendance peut introduire des avertissements d’analyse :

  • Si la nouvelle version a ajouté des modèles de réflexion non compris.
  • Même s’il n’y avait aucune modification de l’API.
  • L’introduction des avertissements d’analyse de découpage est un changement cassant lorsque la bibliothèque est utilisée avec PublishTrimmed.

Résoudre les avertissements de suppression

Les étapes précédentes produisent des avertissements sur le code qui peuvent entraîner des problèmes lorsqu’il est utilisé dans une application ayant inclus des découpages. Les exemples suivants montrent les avertissements les plus courants avec des recommandations pour les corriger.

RequiresUnreferencedCode

Considérez le code suivant qui utilise [RequiresUnreferencedCode] pour indiquer que la méthode spécifiée nécessite un accès dynamique au code qui n’est pas référencé statiquement, par exemple, via 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()
    {
    }
}

Le code mis en surbrillance précédent indique que la bibliothèque appelle une méthode qui a été explicitement annotée comme incompatible avec le découpage. Pour faire disparaître l’avertissement, déterminez si MyMethod a besoin d’appeler DynamicBehavior. Dans ce cas, annotez l’appelant MyMethod avec [RequiresUnreferencedCode] lequel propage l’avertissement afin que les appelants de MyMethod obtiennent un avertissement à la place :

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

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

Une fois que vous avez propagé l’attribut jusqu’à l’API publique, les applications appelant la bibliothèque :

  • Obtiennent des avertissements uniquement pour les méthodes publiques qui ne sont pas modifiables.
  • Ne reçoivent pas d’avertissements comme 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())
        {
            // ...
        }
    }
}

Dans le code précédent, UseMethods appelle une méthode de réflexion qui a une exigence [DynamicallyAccessedMembers]. Cette exigence indique que les méthodes publiques du type sont disponibles. Répondez à l’exigence en ajoutant la même exigence au paramètre de UseMethods.

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

À présent, les appels à UseMethods génèrent des avertissements s’ils transmettent des valeurs qui ne répondent pas à l’exigence PublicMethods. Similaire à [RequiresUnreferencedCode], une fois que vous avez propagé ces avertissements aux API publiques, vous avez terminé.

Dans l’exemple suivant, un type inconnu circule dans le paramètre de méthode annotée. L’inconnu Type provient d’un champ :

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);
}

De même, ici, le problème est que le champ type est passé dans un paramètre avec ces exigences. Elle est corrigée en ajoutant [DynamicallyAccessedMembers] au champ. [DynamicallyAccessedMembers] affiche des avertissements au sujet du code qui affecte des valeurs incompatibles au champ. Parfois, ce processus continue jusqu’à ce qu’une API publique soit annotée, et d’autres fois il se termine quand un type concret arrive dans un emplacement avec ces exigences. Exemple :

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

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

Dans ce cas, l’analyse de découpage conserve simplement les méthodes publiques de Tuple et produit des avertissements supplémentaires.

Recommandations

  • Évitez la réflexion lorsque cela est possible. Lorsque vous utilisez la réflexion, réduisez l’étendue de la réflexion afin qu’elle soit accessible uniquement à partir d’une petite partie de la bibliothèque.
  • Annotez du code avec DynamicallyAccessedMembers pour exprimer statiquement les exigences de découpage lorsque cela est possible.
  • Envisagez de réorganiser le code pour qu’il suive un modèle analysable qui peut être annoté avec DynamicallyAccessedMembers
  • Lorsque le code n’est pas compatible avec le découpage, annotez-le avec RequiresUnreferencedCode et propagez cette annotation aux appelants jusqu’à ce que les API publiques appropriées soient annotées.
  • Évitez d’utiliser du code qui utilise la réflexion d’une manière non comprise par l’analyse statique. Par exemple, la réflexion dans les constructeurs statiques doit être évitée. L’utilisation d’une réflexion statique non analysable dans les constructeurs statiques entraîne la propagation de l’avertissement à tous les membres de la classe.
  • Évitez d’annoter les méthodes virtuelles ou les méthodes d’interface. L’annotation des méthodes virtuelles ou d’interface nécessite que toutes les substitutions aient des annotations correspondantes.
  • Si une API est principalement incompatible, vous devrez peut-être envisager d’autres approches de codage de l’API. Les sérialiseurs basés sur la réflexion en sont un exemple courant. Dans ce cas, envisagez d’adopter d’autres technologies telles que des générateurs sources pour produire du code plus facilement analysé de manière statique. Par exemple, consultez le Guide pratique sur la génération de la source dans System.Text.Json

Résoudre les avertissements pour les modèles non analysables

Il est préférable de résoudre les avertissements en exprimant l’intention de votre code à l’aide de [RequiresUnreferencedCode] et DynamicallyAccessedMembers lorsque cela est possible. Toutefois, dans certains cas, vous pouvez être intéressé par l’activation du découpage pour une bibliothèque qui utilise des modèles qui ne peuvent pas être exprimés avec ces attributs, ou sans refactoriser le code existant. Cette section décrit certaines méthodes avancées pour résoudre les avertissements d’analyse de suppression.

Avertissement

Ces techniques peuvent modifier le comportement ou votre code ou entraîner des exceptions d’exécution si elles sont utilisées de manière incorrecte.

UnconditionalSuppressMessage

Considérez le code que :

  • Son intention ne peut pas être exprimée avec les annotations.
  • Génère un avertissement, mais ne représente pas un problème réel au moment de l’exécution.

Les avertissements peuvent être supprimés UnconditionalSuppressMessageAttribute. Ceci est similaire à l’utilisation de SuppressMessageAttribute, mais ceci est conservé en IL et respecté lors de l’analyse de suppression.

Avertissement

Lors de la suppression d’avertissements, vous êtes responsable de garantir la compatibilité avec le découpage du code en fonction des invariants que vous savez être vrais par les inspections et par les tests. Utilisez ces annotations avec prudence, car si elles sont incorrectes ou si des invariants de votre code changent, elles risquent de masquer des codes incorrects.

Par exemple :

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
{
}

Dans le code précédent, la propriété de l’indexeur a été annotée afin que l’élément Type retourné réponde aux exigences de CreateInstance. Cela garantit que le constructeur TypeWithConstructor est conservé et que l’appel à CreateInstance ne génère pas d’avertissement. L’annotation setter de l’indexeur garantit que tous les types stockés dans Type[] ont un constructeur. Toutefois, l’analyse n’est pas en mesure de voir ceci et génère toujours un avertissement pour l’élément getter, car elle ne sait pas que le type retourné a conservé son constructeur.

Si vous êtes sûr que les exigences sont remplies, vous pouvez désactiver cet avertissement en ajoutant [UnconditionalSuppressMessage] à l’élément getter :

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
{
}

Il est important de souligner que cela n’est valide que pour supprimer un avertissement s’il existe des annotations ou du code qui garantissent que les membres qui ont fait l’objet d’une réflexion sont des cibles visibles de la réflexion. Il ne suffit pas que le membre soit une cible d’un accès à un appel, un champ ou une propriété. Cela peut sembler être le cas parfois, mais ce code finira par être endommagé à mesure que d’autres optimisations de suppression seront ajoutées. Les propriétés, les champs et les méthodes qui ne sont pas des cibles visibles de la réflexion peuvent être inclus, leurs noms peuvent être supprimés, ils peuvent être déplacés vers différents types ou optimisés d’une manière qui endommage le processus de réflexion. Lors de la suppression d’un avertissement, il n’est possible d’effectuer une réflexion qu’envers les cibles qui étaient des cibles visibles de la réflexion appliquée à l’analyseur de suppression ailleurs.

// 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

L’attribut [DynamicDependency] peut être utilisé pour indiquer qu’un membre a une dépendance dynamique sur d’autres membres. Cela entraîne la conservation des membres référencés chaque fois que le membre avec l’attribut est conservé, mais cela n’interrompt pas de facto les avertissements. Contrairement aux autres attributs qui indiquent à l’analyse de découpage sur le comportement de réflexion du code, [DynamicDependency] conserve uniquement les autres membres. Cela peut être utilisé avec [UnconditionalSuppressMessage] pour corriger certains avertissements d’analyse.

Avertissement

Utilisez l’attribut [DynamicDependency] uniquement comme dernier recours lorsque les autres approches ne sont pas viables. Il est préférable d’exprimer le comportement de réflexion à l’aide de [RequiresUnreferencedCode] ou [DynamicallyAccessedMembers].

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

Sans DynamicDependency, la suppression peut supprimer Helper de MyAssembly ou supprimer MyAssembly complètement s’il n’est pas référencé ailleurs, produisant ainsi un avertissement qui indique une défaillance possible au moment de l’exécution. L’attribut garantit que Helper est conservé.

L’attribut spécifie les membres à conserver via string ou via DynamicallyAccessedMemberTypes. Le type et l’assembly sont implicites dans le contexte d’attribut ou spécifiés explicitement dans l’attribut (par Type, ou par string pour le type et le nom de l’assembly).

Les chaînes de type et de membre utilisent une variante du format de chaîne d’ID de commentaire de documentation C#, sans préfixe de membre. La chaîne de membre ne doit pas inclure le nom du type déclarant et peut omettre des paramètres pour conserver tous les membres du nom spécifié. Quelques exemples du format sont présentés dans le code suivant :

[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<>))]

L’attribut [DynamicDependency] est conçu pour être utilisé dans les cas où une méthode contient des modèles de réflexion qui ne peuvent pas être analysés même avec l’aide de DynamicallyAccessedMembersAttribute.