Introduction aux avertissements de découpage

D’un point de vue conceptuel, le découpage est simple : lorsque vous publiez une application, le kit de développement logiciel (SDK) .NET analyse l’ensemble de l’application et supprime tout le code inutilisé. Toutefois, il peut être difficile de déterminer ce qui est inutilisé, ou plus précisément, ce qui est utilisé.

Pour empêcher les modifications du comportement lors du découpage d’applications, le SDK .NET fournit une analyse statique de la compatibilité avec le découpage par le biais des avertissements de découpage. Le découpage produit des avertissements lorsqu'il trouve du code qui pourrait ne pas être compatible avec le découpage. Le code qui n’est pas compatible avec le découpage peut produire des changements de comportement, voire des plantages, dans une application après son découpage. Dans l’idéal, toutes les applications qui utilisent le découpage ne devraient pas produire d’avertissements de découpage. En cas d’avertissements de découpage, l’application doit être testée minutieusement après le découpage pour vérifier qu’il n’y a aucun changement de comportement.

Cet article vous aide à comprendre pourquoi certains modèles génèrent des avertissements de découpage, et comment ces avertissements peuvent être traités.

Exemples d’avertissements de découpage

Pour une majeure partie du code C#, il est simple de déterminer le code utilisé et le code non utilisé : le découpage peut parcourir les appels de méthode, les références de champs et de propriétés, etc., et déterminer le code accessible. Malheureusement, certaines fonctionnalités, comme la réflexion, présentent un problème important. Considérez le code suivant :

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Dans cet exemple, GetType() demande dynamiquement un type avec un nom inconnu, puis imprime les noms de toutes ses méthodes. Étant donné qu’il n’existe aucun moyen de savoir au moment de la publication quel nom de type va être utilisé, il n’existe aucun moyen pour l’outil de découpage de savoir quel type conserver dans la sortie. Il est probable que ce code ait pu fonctionner avant le découpage (tant que l’entrée est connue pour exister dans l’infrastructure cible), mais qu’il produirait probablement une exception de référence null après le découpage, puisque Type.GetType renvoie une valeur null lorsque le type n’est pas trouvé.

Dans ce cas, l’outil de découpage émet un avertissement sur l’appel à Type.GetType, indiquant qu’il ne peut pas déterminer le type qui sera utilisé par l’application.

Réaction aux avertissements de découpage

Les avertissements de découpage sont destinés à apporter de la prévisibilité au découpage. Il existe deux grandes catégories d’avertissements que vous observerez probablement :

  1. La fonctionnalité n’est pas compatible avec le découpage
  2. La fonctionnalité impose certaines exigences à l'entrée pour qu'elle soit compatible avec le découpage

La fonctionnalité est incompatible avec le découpage

Il s'agit généralement de méthodes qui ne fonctionnent pas du tout ou qui peuvent être cassées dans certains cas si elles sont utilisées dans une application découpée. Un bon exemple est la méthode Type.GetType de l’exemple précédent. Dans une application réduite, cela pourrait fonctionner, mais il n'y a aucune garantie. Ces API sont marquées avec RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute est simple et étendu : c’est un attribut qui signifie que le membre a été annoté comme étant incompatible avec le découpage. Cet attribut est utilisé lorsque le code n’est fondamentalement pas compatible avec le découpage, ou lorsque la dépendance de découpage est trop complexe pour être expliquée à l’outil de découpage. Cela serait souvent vrai pour les méthodes qui chargent dynamiquement du code par exemple via LoadFrom(String), énumérer ou rechercher tous les types dans une application ou un assemblage, par exemple via GetType(), utiliser le mot clé C# dynamic ou utiliser d’autres technologies de génération de code runtime. Un exemple serait :

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
    ...
    Assembly.LoadFrom(...);
    ...
}

void TestMethod()
{
    // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
    MethodWithAssemblyLoad();
}

Il n’existe pas beaucoup de solutions de contournement pour RequiresUnreferencedCode. La meilleure solution consiste à éviter d’appeler la méthode lors du découpage et à utiliser quelque chose d’autre qui est compatible avec le découpage.

Marquer la fonctionnalité comme incompatible avec le découpage

Si vous écrivez une bibliothèque et qu’elle n’est pas dans votre contrôle si vous souhaitez ou non utiliser des fonctionnalités incompatibles, vous pouvez la marquer avec RequiresUnreferencedCode. Cela indique que votre méthode est incompatible avec l'élagage. L’utilisation de RequiresUnreferencedCode a pour effet d’ignorer tous les avertissements dans la méthode donnée, mais produit un avertissement chaque fois que quelqu’un d’autre l’appelle.

Le RequiresUnreferencedCodeAttribute vous oblige à spécifier un Message. Le message est affiché dans le cadre d'un avertissement signalé au développeur qui appelle la méthode marquée. Par exemple :

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

Dans l'exemple ci-dessus, un avertissement pour une méthode spécifique pourrait ressembler à ceci :

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

Les développeurs qui appellent ces API ne s'intéressent généralement pas aux particularités de l'API concernée ou aux spécificités liées au décapage.

Un bon message doit indiquer les fonctionnalités qui ne sont pas compatibles avec le découpage et guider le développeur sur les prochaines étapes potentielles. Il peut suggérer d'utiliser une fonctionnalité différente ou de modifier la manière dont la fonctionnalité est utilisée. Il peut aussi simplement indiquer que la fonctionnalité n'est pas encore compatible avec l'élagage sans qu'il y ait un remplacement clair.

Si les conseils du développeur deviennent trop longs pour être inclus dans un message d’avertissement, vous pouvez ajouter un Url facultatif pour le RequiresUnreferencedCodeAttribute pour pointer le développeur vers une page web décrivant le problème et les solutions possibles plus en détail.

Par exemple :

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

Un avertissement est alors émis :

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method

L’utilisation de RequiresUnreferencedCode conduit souvent à marquer davantage de méthodes avec elle, en raison de la même raison. Ce cas est fréquent lorsqu'une méthode de haut niveau devient incompatible avec le découpage parce qu'elle appelle une méthode de bas niveau qui n'est pas compatible avec le découpage. L’avertissement est transmis à une API publique. Chaque utilisation d’un RequiresUnreferencedCode nécessite un message et, dans ces cas, les messages sont probablement identiques. Pour éviter de dupliquer les chaînes de caractères et faciliter la maintenance, utilisez un champ constant de type chaîne de caractères pour stocker le message :

class Functionality
{
    const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    private void ImplementationOfAssemblyLoading()
    {
        ...
    }

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    public void MethodWithAssemblyLoad()
    {
        ImplementationOfAssemblyLoading();
    }
}

Fonctionnalités avec exigences sur son entrée

Le découpage fournit des API permettant de spécifier davantage d'exigences sur l'entrée des méthodes et d'autres membres qui conduisent à un code compatible avec le découpage. Ces exigences concernent généralement la réflexion et la possibilité d'accéder à certains membres ou opérations sur un type. Ces exigences sont spécifiées à l’aide du DynamicallyAccessedMembersAttribute.

Contrairement à RequiresUnreferencedCode, la réflexion peut parfois être comprise par l’outil de découpage tant qu’elle est correctement annotée. Passons à nouveau en revue l’exemple d’origine :

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Dans l’exemple précédent, le vrai problème est Console.ReadLine(). N’importe quel type pouvant être lu, le découpage n’a aucun moyen de savoir si vous avez besoin de méthodes sur System.DateTime ou System.Guid tout autre type. En revanche, le code suivant serait correct :

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Ici, l’outil de découpage peut consulter le type exact référencé : System.DateTime. Il peut désormais utiliser l’analyse de flux pour déterminer qu’il doit conserver toutes les méthodes publiques sur System.DateTime. Alors, où DynamicallyAccessMembers intervient-il ? Lorsque la réflexion est répartie entre plusieurs méthodes. Dans le code suivant, nous pouvons voir que le type System.DateTime circule vers Method3 où la réflexion est utilisée pour accéder aux méthodes de System.DateTime,

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

Si vous compilez le code précédent, l'avertissement suivant apparaît :

IL2070 : Program.Method3(Type) : l’argument « this » ne satisfait pas « DynamicallyAccessedMemberTypes.PublicMethods » dans l’appel à « System.Type.GetMethods() ». Le paramètre « type » de la méthode « Program.Method3(Type) » n’a pas d’annotations correspondantes. La valeur source doit déclarer au moins les mêmes exigences que celles déclarées sur l’emplacement cible auquel elle est affectée.

À des fins de performances et de stabilité, l’analyse de flux n’est pas effectuée entre les méthodes. Une annotation est donc nécessaire pour transférer des informations entre les méthodes, de l’appel de réflexion (GetMethods) à la source du Type. Dans l’exemple précédent, l’avertissement du découpage indique que GetMethods nécessite l’instance d’objet Type sur laquelle il est appelé pour avoir l’annotation PublicMethods. Cependant, la variable type n’a pas la même exigence. En d’autres termes, nous devons transmettre les exigences de GetMethods à l’appelant :

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

Après avoir annoté le paramètre type, l’avertissement d’origine disparaît, mais un autre s’affiche :

IL2087 : l’argument « type » ne satisfait pas « DynamicallyAccessedMemberTypes.PublicMethods » dans l’appel à « Program.Method3(Type) ». Le paramètre générique « T » de « Program.Method2<T>() » n’a pas d’annotations correspondantes.

Nous avons propagé les annotations jusqu’au paramètre type de Method3. Dans Method2, le problème est similaire. L’outil de découpage est en mesure de surveiller la valeur T à mesure qu’elle passe par l’appel à typeof, est affectée à la variable locale t, et transférée à Method3. À ce stade, il constate que le paramètre type nécessite PublicMethods, mais qu’il n’y a aucune exigence sur T, et génère un nouvel avertissement. Pour résoudre ce problème, nous devons « annoter et propager » en appliquant des annotations sur l’ensemble de la chaîne d’appels jusqu’à ce que nous atteignions un type statiquement connu (comme System.DateTime ou System.Tuple) ou une autre valeur annotée. Dans ce cas, nous devons annoter le paramètre de type T de Method2.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

À présent, aucun avertissement n’est détecté, car le découpage sait quels membres peuvent être accessibles via la réflexion de runtime (méthodes publiques) et sur quels types (System.DateTime), et les conserve. La meilleure pratique consiste à ajouter des annotations afin que le découpage sache ce qu'il doit conserver.

Les avertissements générés par ces exigences supplémentaires sont automatiquement supprimés si le code affecté se trouve dans une méthode avec RequiresUnreferencedCode.

Contrairement à RequiresUnreferencedCode, ce qui signale simplement l’incompatibilité, l’ajout de DynamicallyAccessedMembers rend le code compatible avec le découpage.

Suppression des avertissements relatifs au découpage

Si, d’une manière ou d’une autre, vous pouvez déterminer que l’appel est sécurisé et que l’ensemble du code nécessaire ne sera pas découpé, vous pouvez également supprimer l’avertissement à l’aide de UnconditionalSuppressMessageAttribute. Par exemple :

[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
    Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
    InitializeEverything();

    MethodWithAssemblyLoad(); // Warning suppressed

    ReportResults();
}

Avertissement

Soyez très prudent quand vous supprimez des avertissements relatifs au découpage. Il est possible que l’appel soit désormais compatible avec le découpage, mais à mesure que vous modifiez votre code qui peut changer, vous pouvez oublier de passer en revue toutes les suppressions.

UnconditionalSuppressMessage est comme SuppressMessage, mais il peut être vu par publish et par d’autres outils post-build.

Important

N’utilisez pas SuppressMessage ou #pragma warning disable pour supprimer les avertissements de découpage. Ils ne fonctionnent que pour le compilateur, mais ne sont pas conservés dans l'assemblage compilé. Le découpage opère sur des assemblages compilés et ne verrait pas la suppression.

La suppression s'applique à l'ensemble du corps de la méthode. Ainsi, dans notre exemple ci-dessus, il supprime tous les avertissements IL2026 de la méthode. Cela rend les choses plus difficiles à comprendre, car il n'est pas évident de savoir quelle méthode pose problème, à moins que vous n'ajoutiez un commentaire. Plus important encore, si le code change à l’avenir, par exemple si ReportResults devient incompatible, aucun avertissement n’est signalé pour cet appel de méthode.

Vous pouvez résoudre ce problème en transformant l'appel de méthode problématique en une méthode distincte ou en une fonction locale, puis en appliquant la suppression à cette seule méthode :

void TestMethod()
{
    InitializeEverything();

    CallMethodWithAssemblyLoad();

    ReportResults();

    [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
    void CallMethodWithAssemblyLoad()
    {
        MethodWIthAssemblyLoad(); // Warning suppressed
    }
}