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 susceptibles d’interrompre les applications réduites. Cet article :
- Décrit comment préparer des bibliothèques pour le découpage.
- Fournit des recommandations pour résoudre les avertissements de découpage courants.
Prérequis
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.
- 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>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 de l’implémentation des 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’ils sont utilisés dans une application rogné. 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 d’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 par 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 n’est pas suffisant que le membre ait été une cible d’accès à un appel, à un champ ou à une propriété. Il peut sembler être le cas parfois, mais ce code est lié à l’arrêt à mesure que d’autres optimisations de découpage sont 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 inline, ont leurs noms supprimés, sont déplacés vers différents types ou sont optimisés de manière à ce qu’ils se reflètent. 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 les 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
.