Condividi tramite


Preparare le librerie .NET per il trimming

.NET SDK consente di ridurre le dimensioni delle app autonome mediante il trimming. Il trimming rimuove il codice inutilizzato dall'app e dalle relative dipendenze. Non tutto il codice è compatibile con il trimming. .NET fornisce avvisi di analisi del trimming per rilevare modelli che potrebbero interrompere l’esecuzione delle app sottoposte a trimming. Questo articolo:

Prerequisiti

.NET 6 SDK o versioni successive.

Per ottenere gli avvisi di trimming più aggiornati e la copertura dell'analizzatore:

  • Installare e usare .NET 8 SDK o versioni successive.
  • Impostare come destinazione net8.0 o versioni successive.

.NET 7 SDK o versioni successive.

Per ottenere gli avvisi di trimming più aggiornati e la copertura dell'analizzatore:

  • Installare e usare .NET 8 SDK o versioni successive.
  • Impostare come destinazione net8.0 o versioni successive.

.NET 8 SDK o versioni successive.

Abilitare gli avvisi di trimming della libreria

Per trovare gli avvisi di trimming in una libreria è possibile usare uno dei metodi seguenti:

  • Abilitare il trimming specifico del progetto tramite la proprietà IsTrimmable.
  • Creare un'app di test del trimming che usa la libreria e abilitare il trimming per l'app di test. Non è necessario fare riferimento a tutte le API della libreria.

È consigliabile usare entrambi gli approcci. Il trimming specifico del progetto è un metodo pratico che mostra gli avvisi di trimming per un singolo progetto, ma si basa sui riferimenti contrassegnati come compatibili con il trimming per visualizzare tutti gli avvisi. La creazione di un'app di test del trimming richiede più lavoro, ma mostra tutti gli avvisi.

Abilitare il trimming specifico del progetto

Impostare <IsTrimmable>true</IsTrimmable> nel file di progetto.

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

L'impostazione della proprietà MSBuild IsTrimmable su true contrassegna l'assembly come "trimmable" e abilita gli avvisi di trimming. La proprietà "IsTrimmable" indica che il progetto:

  • È considerato compatibile con il trimming.
  • Non dovrebbe generare avvisi relativi al trimming durante la compilazione. Quando l’assembly viene usato in un'app sottoposta a trimming, i relativi membri inutilizzati verranno rimossi dall'output finale.

Per impostazione predefinita, la proprietà IsTrimmable viene impostata su true quando si configura un progetto come compatibile con AOT con <IsAotCompatible>true</IsAotCompatible>. Per altre informazioni, vedere Analizzatori di compatibilità AOT.

Per generare avvisi di trimming senza contrassegnare il progetto come compatibile con il trimming, usare <EnableTrimAnalyzer>true</EnableTrimAnalyzer> al posto di <IsTrimmable>true</IsTrimmable>.

Visualizzare tutti gli avvisi con l'app di test

Per visualizzare tutti gli avvisi di analisi per una libreria, il trimmer deve analizzare l'implementazione della libreria e di tutte le dipendenze usate da quest’ultima.

Durante la compilazione e la pubblicazione di una libreria:

  • Le implementazioni delle dipendenze non sono disponibili.
  • Gli assembly di riferimento disponibili non dispongono di informazioni sufficienti per consentire al trimmer di determinare se sono compatibili con il trimming.

A causa delle limitazioni delle dipendenze, è necessario creare un'app di test autonoma che usa la libreria e le relative dipendenze. L'app di test include tutte le informazioni di cui il trimmer ha bisogno per generare un avviso in caso di incompatibilità con il trimming nel codice seguente:

  • Codice della libreria.
  • Codice a cui la libreria fa riferimento dalle relative dipendenze.

Nota

Se la libreria ha un comportamento diverso a seconda del framework di destinazione, creare un'app di test del trimming per ognuno dei framework di destinazione che supportano il trimming. Un esempio è il caso in cui la libreria usa la compilazione condizionale, come #if NET7_0, per modificare il comportamento.

Per creare l'app di test del trimming:

  • Creare un progetto di applicazione console distinto.
  • Aggiungere un riferimento alla libreria.
  • Modificare il progetto in modo simile a quello illustrato di seguito usando l'elenco seguente:

Se la libreria è destinata a un TFM non compatibile con il trimming, ad esempio net472 o netstandard2.0, la creazione di un'app di test del trimming non offre alcun vantaggio. Il trimming è supportato solo per .NET 6 e versioni successive.

  • Impostare <TrimmerDefaultAction> su link.
  • Aggiungere <PublishTrimmed>true</PublishTrimmed>.
  • Aggiungere un riferimento al progetto di libreria con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Specificare la libreria come assembly radice del trimmer con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly assicura che venga analizzata ogni parte della libreria. Indica al trimmer che questo assembly è una "radice". Questo significa che il trimmer analizza ogni chiamata nella libreria e attraversa tutti i percorsi del codice che hanno origine da tale assembly.
  • Aggiungere <PublishTrimmed>true</PublishTrimmed>.
  • Aggiungere un riferimento al progetto di libreria con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Specificare la libreria come assembly radice del trimmer con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly assicura che venga analizzata ogni parte della libreria. Indica al trimmer che questo assembly è una "radice". Questo significa che il trimmer analizza ogni chiamata nella libreria e attraversa tutti i percorsi del codice che hanno origine da tale assembly.
  • Aggiungere <PublishTrimmed>true</PublishTrimmed>.
  • Aggiungere un riferimento al progetto di libreria con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Specificare la libreria come assembly radice del trimmer con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly assicura che venga analizzata ogni parte della libreria. Indica al trimmer che questo assembly è una "radice". Questo significa che il trimmer analizza ogni chiamata nella libreria e attraversa tutti i percorsi del codice che hanno origine da tale assembly.

File con estensione csproj

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.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>

Nota: se si usa .NET 7, nel file di progetto precedente sostituire <TargetFramework>net8.0</TargetFramework> con <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>

Dopo aver aggiornato il file di progetto, eseguire dotnet publish con l'identificatore di runtime (RID) di destinazione.

dotnet publish -c Release -r <RID>

Seguire lo stesso modello per più librerie. Per visualizzare gli avvisi di analisi del trimming per più librerie contemporaneamente, aggiungerle tutte allo stesso progetto come elementi ProjectReference e TrimmerRootAssembly. L'aggiunta di tutte le librerie allo stesso progetto con gli elementi ProjectReference e TrimmerRootAssembly genera avvisi sulle dipendenze se una qualsiasi delle librerie radice usa un'API non compatibile con il trimming in una dipendenza. Per visualizzare gli avvisi che riguardano solo una determinata libreria, fare riferimento solo a quella libreria.

Nota: i risultati dell'analisi dipendono dai dettagli di implementazione delle dipendenze. L'aggiornamento a una nuova versione di una dipendenza può introdurre avvisi di analisi:

  • Se la nuova versione ha aggiunto modelli di reflection non compresi.
  • Anche se non sono state apportate modifiche alle API.
  • L'introduzione degli avvisi di analisi del trimming è una modifica che causa un'interruzione quando la libreria viene usata con PublishTrimmed.

Risolvere gli avvisi di trimming

I passaggi precedenti generano avvisi relativi al codice che possono causare problemi quando vengono usati in un'app sottoposta a trimming. Gli esempi seguenti mostrano gli avvisi più comuni con consigli su come risolverli.

RequiresUnreferencedCode

Esaminare il codice seguente che usa [RequiresUnreferencedCode] per indicare che il metodo specificato richiede l'accesso dinamico a codice a cui non viene fatto riferimento in modo statico, ad esempio tramite 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()
    {
    }
}

Il codice evidenziato riportato sopra indica che la libreria chiama un metodo che è stato annotato in modo esplicito come incompatibile con il trimming. Per eliminare l'avviso, stabilire se MyMethod deve chiamare DynamicBehavior. In caso affermativo, annotare il metodo MyMethod del chiamante con [RequiresUnreferencedCode], che propaga l'avviso in modo che i chiamanti di MyMethod ricevano un avviso:

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

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

Una volta propagato l'attributo fino all'API pubblica, le app che chiamano la libreria:

  • Ricevono avvisi solo per i metodi pubblici che non sono compatibili con il trimming.
  • Non ricevono avvisi come 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())
        {
            // ...
        }
    }
}

Nel codice precedente, UseMethods chiama un metodo di reflection che ha un requisito [DynamicallyAccessedMembers], che indica che sono disponibili i metodi pubblici del tipo. Per soddisfare il requisito, aggiungere lo stesso requisito al parametro di UseMethods.

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

In questo modo tutte le chiamate a UseMethods genereranno avvisi se passano valori che non soddisfano il requisito PublicMethods. Analogamente a [RequiresUnreferencedCode], una volta propagati questi avvisi alle API pubbliche, la procedura è completata.

Nell'esempio seguente, un elemento Type sconosciuto viene passato nel parametro del metodo annotato. L'elemento Type sconosciuto proviene da un campo:

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

Anche in questo caso, il problema è dato dal fatto che il campo type viene passato in un parametro con questi requisiti. Per correggere il problema, aggiungere [DynamicallyAccessedMembers] al campo. [DynamicallyAccessedMembers] genera avvisi relativi al codice che assegna valori incompatibili al campo. A volte questo processo continua fino a quando non viene annotata un'API pubblica, mentre a volte termina quando un tipo concreto viene passato in una posizione con questi requisiti. Ad esempio:

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

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

In questo caso, l'analisi del trimming mantiene i metodi pubblici di Tuplee genera ulteriori avvisi.

Consigli

  • Laddove possibile, evitare la reflection. Se la si usa, ridurre al minimo l'ambito di reflection in modo che sia raggiungibile solo da una piccola parte della libreria.
  • Annotare il codice con DynamicallyAccessedMembers per esprimere in modo statico i requisiti di trimming, quando possibile.
  • Valutare la riorganizzazione del codice per renderlo conforme a un modello analizzabile che può essere annotato con DynamicallyAccessedMembers.
  • Quando il codice non è compatibile con il trimming, annotarlo con RequiresUnreferencedCode e propagare l’annotazione ai chiamanti finché le API pubbliche pertinenti non vengono annotate.
  • Evitare di usare codice che usa la reflection in un modo che l'analisi statica non è in grado di comprendere. Ad esempio, è opportuno evitare la reflection nei costruttori statici. L'uso di un processo di reflection non analizzabile in modo statico nei costruttori statici comporta la propagazione degli avvisi a tutti i membri della classe.
  • Evitare di annotare metodi virtuali o metodi di interfaccia. Per annotare questi tipi di metodi è necessario che tutti gli override abbiano annotazioni corrispondenti.
  • Se un'API è principalmente non compatibile con il trimming, potrebbe essere necessario prendere in considerazione approcci di codifica alternativi all'API. Un esempio comune è costituito dai serializzatori basati su reflection. In questi casi, valutare l'adozione di altre tecnologie, come i generatori di origini, per produrre codice che possa essere più facilmente analizzato in modo statico. Ad esempio, vedere Come usare la generazione di origini in System.Text.Json.

Risolvere gli avvisi per i modelli non analizzabili

Laddove possibile, è preferibile risolvere gli avvisi esprimendo la finalità del codice tramite [RequiresUnreferencedCode] e DynamicallyAccessedMembers. In alcuni casi, tuttavia, potrebbe essere utile abilitare il trimming di una libreria che usa modelli che non possono essere espressi con questi attributi o senza effettuare il refactoring del codice esistente. Questa sezione descrive alcune modalità avanzate di risoluzione degli avvisi di analisi del trimming.

Avviso

Se usate in modo non corretto, queste tecniche potrebbero modificare il comportamento del codice o generare eccezioni in fase di esecuzione.

UnconditionalSuppressMessage

Si consideri codice che:

  • Ha una finalità che non può essere espressa con le annotazioni.
  • Genera un avviso ma non rappresenta un problema reale in fase di esecuzione.

Gli avvisi possono essere eliminati con l’attributo UnconditionalSuppressMessageAttribute. È simile a SuppressMessageAttribute, ma è persistente in IL e viene rispettato durante l'analisi del trimming.

Avviso

Quando si eliminano gli avvisi, si ha la responsabilità di garantire la compatibilità del codice con il trimming in base a invarianti che sono state confermate come vere dalle attività di analisi e test. Usare queste annotazioni con cautela, in quanto se non sono corrette o se le invarianti del codice cambiano, potrebbero finire per nascondere codice non corretto.

Ad esempio:

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

In questo codice la proprietà dell'indicizzatore è stata annotata in modo che l'elemento Type restituito soddisfi i requisiti di CreateInstance. Questo assicura che il costruttore TypeWithConstructor venga mantenuto e che la chiamata a CreateInstance non generi avvisi. L'annotazione del setter dell'indicizzatore garantisce che qualsiasi tipo archiviato in Type[] disponga di un costruttore. Tuttavia, l'analisi non è in grado di verificarlo e genera un avviso per il getter, perché non sa che il tipo restituito ha mantenuto il relativo costruttore.

Se si è certi che i requisiti sono soddisfatti, è possibile disattivare questo avviso aggiungendo [UnconditionalSuppressMessage] al 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
{
}

È importante sottolineare che è consentito eliminare un avviso solo se sono presenti annotazioni o codice che assicurano che i membri con reflection siano destinazioni visibili della reflection. Non è sufficiente che il membro fosse una destinazione di una chiamata, di un campo o di un accesso a proprietà. A volte può sembrare che sia così, ma questo tipo di codice è destinato a smettere di funzionare man mano che vengono aggiunte ulteriori ottimizzazioni del trimming. Le proprietà, i campi e i metodi che non sono destinazioni visibili della reflection potrebbero essere resi inline, privati del nome, essere spostati in tipi diversi o essere altrimenti ottimizzati in modo da interrompere la reflection. Quando si elimina un avviso, è consentito applicare la reflection solo alle destinazioni che erano destinazioni visibili di reflection per l'analizzatore del trimming in un’altra posizione.

// 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'attributo [DynamicDependency] può essere usato per indicare che un membro ha una dipendenza dinamica da altri membri. Questo fa sì che i membri a cui si fa riferimento vengano mantenuti ogni volta che viene mantenuto il membro con l'attributo, ma non disattiva gli avvisi di per sé. A differenza degli altri attributi, che informano l'analisi del trimming sul comportamento di reflection del codice, [DynamicDependency] mantiene solo altri membri. Questo attributo può essere usato insieme a [UnconditionalSuppressMessage] per risolvere alcuni avvisi di analisi.

Avviso

Usare l'attributo [DynamicDependency] solo come ultima risorsa quando gli altri approcci non sono applicabili. È preferibile esprimere il comportamento di reflection mediante [RequiresUnreferencedCode] o [DynamicallyAccessedMembers].

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

Senza DynamicDependency, il trimming potrebbe rimuovere Helper da MyAssembly o rimuovere MyAssembly completamente se non vi viene fatto riferimento altrove, generando un avviso che indica un possibile errore in fase di esecuzione. L'attributo assicura che Helper venga mantenuto.

L'attributo specifica i membri da mantenere tramite un tipo string o un DynamicallyAccessedMemberTypes. Il tipo e l'assembly sono impliciti nel contesto dell'attributo oppure specificati in modo esplicito nell'attributo (da Typeo da string per il tipo e il nome dell'assembly).

Le stringhe di tipo e membro usano una variante del formato di stringa dell’ID commento della documentazione di C#, senza il prefisso del membro. La stringa del membro non deve includere il nome del tipo dichiarante e può omettere i parametri per mantenere tutti i membri del nome specificato. Il codice seguente illustra alcuni esempi del formato:

[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'attributo [DynamicDependency] è progettato per essere usato nei casi in cui un metodo contiene modelli di reflection che non possono essere analizzati neanche con l'aiuto di DynamicallyAccessedMembersAttribute.