Vorbereiten von .NET-Bibliotheken für Kürzungen

Mit dem .NET SDK können Sie die Größe eigenständiger Apps durch Kürzen verringern. Durch das Kürzen werden nicht verwendeter Code aus der App und deren Abhängigkeiten entfernt. Nicht der gesamte Code ist kürzungskompatibel. .NET stellt Warnungen zur Kürzung von Analysen bereit, um Muster zu erkennen, die gekürzte Apps möglicherweise unterbrechen. Dieser Artikel:

Voraussetzungen

.NET 8 SDK oder höher.

Aktivieren von Kürzungswarnungen für Bibliotheken

Kürzungswarnungen in einer Bibliothek können mit einer der folgenden Methoden gefunden werden:

  • Durch Aktivieren der projektspezifische Kürzung mithilfe der IsTrimmable-Eigenschaft.
  • Erstellen einer Kürzungstest-App, die die Bibliothek verwendet und das Kürzen für die Test-App aktiviert. Es ist nicht erforderlich, auf alle APIs in der Bibliothek zu verweisen.

Es wird empfohlen, beide Ansätze zu verwenden. Projektspezifisches Kürzen ist praktisch, und es werden Kürzungswarnungen für ein Projekt angezeigt. Diese Option ist jedoch von den Verweisen abhängig, die als kürzungskompatibel gekennzeichnet werden, um alle Warnungen anzuzeigen. Das Kürzen einer Test-App ist arbeitsintensiver, zeigt jedoch alle Warnungen an.

Aktivieren des projektspezifischen Kürzens

Legen Sie <IsTrimmable>true</IsTrimmable> in der Projektdatei fest.

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

Wenn Sie die Eigenschaft MSBuild IsTrimmable auf true setzen, wird die Assembly als „kürzbar“ markiert und Warnhinweise werden aktiviert. Kürzbar bedeutet, dass das Projekt:

  • Als kürzungskompatibel erachtet wird.
  • Beim Erstellen keine kürzungsbezogenen Warnungen generieren sollte. Wenn die Assembly in einer gekürzten App verwendet wird, werden die nicht verwendeten Member in der finalen Ausgabe gekürzt.

Die IsTrimmable-Eigenschaft wird standardmäßig auf true festgelegt, wenn ein Projekt als AOT-kompatibel mit <IsAotCompatible>true</IsAotCompatible> konfiguriert wird. Weitere Informationen finden Sie unter AOT-Kompatibilitätsanalyse.

Verwenden Sie <EnableTrimAnalyzer>true</EnableTrimAnalyzer> anstelle von <IsTrimmable>true</IsTrimmable>, um Kürzungswarnungen zu generieren, ohne das Projekt als kürzungskompatibel zu kennzeichnen.

Anzeigen aller Warnungen mit der Test-App

Um alle Analysewarnungen für eine Bibliothek anzuzeigen, muss der Trimmer die Implementierung der Bibliothek und aller von der Bibliothek verwendeten Abhängigkeiten analysieren.

Beim Erstellen und Veröffentlichen einer Bibliothek:

  • Die Implementierungen der Abhängigkeiten sind nicht verfügbar.
  • Die verfügbaren Verweisassemblys verfügen nicht über genügend Informationen für den Trimmer, um festzustellen, ob sie kürzungskompatibel sind.

Aufgrund der Abhängigkeitsbeschränkungen muss eine eigenständige Test-App, die die Bibliothek und ihre Abhängigkeiten verwendet, erstellt werden. Die Test-App enthält alle Informationen, die der Trimmer benötigt, um Warnungen bei Inkompatibilitäten in Bezug auf die Kürzung auszustellen:

  • Den Bibliothekscode.
  • Den Code, auf den die Bibliothek von ihren Abhängigkeiten aus verweist.

Hinweis

Wenn die Bibliothek je nach Zielframework ein anderes Verhalten aufweist, erstellen Sie eine Testanwendung zum Kürzen für jedes der Zielframeworks, die das Kürzen unterstützen. Wenn die Bibliothek beispielsweise bedingte Kompilierung verwendet, z. B. #if NET7_0, um das Verhalten zu ändern.

So erstellen Sie die Test-App zum Kürzen:

  • Erstellen Sie ein separates Konsolenanwendungsprojekt.
  • Fügen Sie einen Verweis auf die -Bibliothek hinzu.
  • Ändern Sie das Projekt ähnlich wie das nachfolgende Projekt anhand der folgenden Liste:

Wenn die Bibliothek auf ein TFM ausgerichtet ist, das nicht gekürzt werden kann, z. B. net472 oder netstandard2.0, bietet das Erstellen einer Test-App zum Kürzen keinen Vorteil. Das Kürzen wird nur für .NET 6 oder höher unterstützt.

  • Fügen Sie <PublishTrimmed>true</PublishTrimmed>hinzu.
  • Fügen Sie mit <ProjectReference Include="/Path/To/YourLibrary.csproj" /> einen Verweis auf das Bibliotheksprojekt hinzu.
  • Geben Sie die Bibliothek als Trimmer-Stammassembly mit <TrimmerRootAssembly Include="YourLibraryName" /> an.
    • Mit TrimmerRootAssembly wird sichergestellt, dass alle Teile der Bibliothek analysiert werden. Dem Trimmer wird mitgeteilt, dass es sich bei dieser Assembly um einen „Stamm“ (Root) handelt. Eine Stammassembly bedeutet, dass der Trimmer jeden Aufruf in der Bibliothek analysiert und alle Codepfade durchläuft, die von dieser Assembly stammen.

.csproj-Datei

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

Nachdem die Projektdatei aktualisiert wurde, führen Sie dotnet publish mit dem Ziel-Runtimebezeichner (RID) aus.

dotnet publish -c Release -r <RID>

Verwenden Sie das oben beschriebene Muster für mehrere Bibliotheken. Um Kürzungsanalysewarnungen für mehr als eine Bibliothek gleichzeitig anzuzeigen, fügen Sie sie alle demselben Projekt als ProjectReference- und TrimmerRootAssembly-Elemente hinzu. Das Hinzufügen aller Bibliotheken zum selben Projekt mit ProjectReference-und TrimmerRootAssembly-Elementen warnt vor Abhängigkeiten, wenn eine der Stammbibliotheken eine nicht kürzungskompatible API in einer Abhängigkeit verwendet. Wenn Sie nur Warnungen zu einer bestimmten Bibliothek erhalten möchten, verweisen Sie nur auf diese Bibliothek.

Hinweis

Die Analyseergebnisse hängen von den Implementierungsdetails der Abhängigkeiten ab. Das Aktualisieren auf eine neue Version einer Abhängigkeit kann zu Analysewarnungen führen:

  • Wenn die neue Version nicht verstandene Reflexionsmuster hinzugefügt hat.
  • Auch wenn keine API-Änderungen vorgenommen wurden.
  • Die Einführung von Kürzungsanalysewarnungen ist eine wesentliche Änderung, wenn die Bibliothek mit PublishTrimmed verwendet wird.

Auflösen von Kürzungswarnungen

In den vorherigen Schritten werden Warnungen zu Code erzeugt, die probleme verursachen können, wenn sie in einer gekürzten App verwendet werden. Die folgenden Beispiele zeigen die häufigsten Warnungen mit Empfehlungen zum Beheben dieser Warnungen.

RequiresUnreferencedCode

Beachten Sie den folgenden Code, der [RequiresUnreferencedCode] verwendet, um anzugeben, dass die angegebene Methode einen dynamischen Zugriff auf Code benötigt, der nicht statisch verwiesen wird, z. B. durch 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()
    {
    }
}

Der oben hervorgehobene Code gibt an, dass die Bibliothek eine Methode aufruft, die explizit als nicht kompatibel mit dem Kürzen gekennzeichnet wurde. Um sich der Warnung zu entledigen, überlegen Sie, ob MyMethod wirklich DynamicBehavior aufrufen muss. Ist dies der Fall, versehen Sie den Aufrufer von MyMethod mit [RequiresUnreferencedCode]. Dadurch wird die Warnung weitergegeben, und stattdessen erhalten die Aufrufer von MyMethod eine Warnung:

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

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

Nachdem Sie das Attribut nach oben an die öffentliche API weitergegeben haben, gilt für Apps, die die Bibliothek aufrufen, Folgendes:

  • Sie erhalten Sie Warnungen nur für öffentliche Methoden, die nicht gekürzt werden können.
  • Sie erhalten Sie keine Warnungen wie 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())
        {
            // ...
        }
    }
}

Im vorherigen Code ruft UseMethods eine Reflexionsmethode auf, die eine [DynamicallyAccessedMembers]-Anforderung aufweist. Diese erfordert, dass die öffentlichen Methoden des Typs verfügbar sind. Fügen Sie in diesem Fall dem Parameter von UseMethods dieselbe Anforderung hinzu, um der Anforderung zu genügen.

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

Nun erzeugen alle Aufrufe von UseMethods Warnungen, wenn Werte übergeben werden, die die PublicMethods-Anforderung nicht erfüllen. Ähnlich wie bei [RequiresUnreferencedCode]: Nachdem Sie solche Warnungen nach oben an öffentliche APIs weitergegeben haben, sind Sie fertig.

Im folgenden Beispiel fließt ein unbekannter Typ in den annotierten Methodenparameter. Der unbekannte Type stammt aus einem Feld:

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

Auch hier besteht das Problem darin, dass das Feld type an einen Parameter mit diesen Anforderungen übergeben wird. Fügen Sie dem Feld [DynamicallyAccessedMembers] hinzu, um das Problem zu beheben. Dadurch warnt [DynamicallyAccessedMembers] vor Code, der dem Feld inkompatible Werte zuweist. Dieser Vorgang wird gelegentlich fortgesetzt, bis eine öffentliche API mit einer Anmerkung versehen wird. In anderen Fällen wird er fortgesetzt, bis ein konkreter Typ an eine Stelle mit diesen Anforderungen gelangt. Zum Beispiel:

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

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

In diesem Fall werden bei der Kürzungsanalyse die öffentlichen Methoden von Tuple beibehalten, und es werden keine weiteren Warnungen erzeugt.

Empfehlungen

  • vermeiden Sie nach Möglichkeit Reflexion. Minimieren Sie bei Verwendung der Reflexion den Reflexionsbereich, damit er nur von einem kleinen Teil der Bibliothek erreicht werden kann.
  • Kommentieren Sie Code mit DynamicallyAccessedMembers, um die Kürzungsanforderungen nach Möglichkeit statisch auszudrücken.
  • Erwägen Sie eine Reorganisation des Codes, damit er einem analysierbaren Muster folgt, das mit DynamicallyAccessedMembers kommentiert werden kann.
  • Wenn Code nicht kürzungskompatibel ist, kommentieren Sie ihn mit RequiresUnreferencedCode, und geben Sie diese Anmerkung an Aufrufer weiter, bis die relevanten öffentlichen APIs kommentiert werden.
  • Vermeiden Sie die Verwendung von Code, die Reflexion in einer Weise verwendet, die von der statischen Analyse nicht verstanden wird. So sollte beispielsweise Reflexion in statischen Konstruktoren vermieden werden. Die Verwendung statisch nicht analysierbarer Reflexion in statischen Konstruktoren führt dazu, dass die Warnung an alle Member der Klasse weitergegeben wird.
  • Vermeiden Sie Anmerkungen zu virtuellen Methoden oder Schnittstellenmethoden. Das Versehen von virtuellen Methoden oder Schnittstellenmethoden in Anmerkungen erfordert, dass alle Überschreibungen über übereinstimmende Anmerkungen verfügen.
  • Wenn eine API hauptsächlich inkompatibel ist, müssen möglicherweise alternative Codierungsansätze für die API berücksichtigt werden. Ein gängiges Beispiel hierfür sind reflexionsbasierte Serialisierer. Überprüfen Sie in diesem Fall, ob Sie den Code mit anderen Technologien wie etwa Quellgeneratoren erzeugen können, damit er einfacher statisch analysiert werden kann. Weitere Informationen finden Sie beispielsweise unter Verwenden der Quellgenerierung in System.Text.Json

Auflösen von Warnungen für nicht analysierbare Muster

Es ist vorteilhafter, Warnungen dadurch zu lösen, dass Sie den Zweck Ihres Codes möglichst mithilfe von [RequiresUnreferencedCode] und DynamicallyAccessedMembers ausdrücken. In einigen Fällen können Sie jedoch daran interessiert sein, das Kürzen einer Bibliothek zu aktivieren, die Muster verwendet, die nicht mit diesen Attributen ausgedrückt werden können, oder ohne vorhandenen Code umzugestalten. In diesem Abschnitt werden einige Methoden zur Auflösung von Kürzungsanalysewarnungen beschrieben.

Warnung

Diese Techniken können das Verhalten oder den Code ändern oder zu Laufzeitausnahmen führen, wenn sie falsch verwendet werden.

UnconditionalSuppressMessage

Achten Sie auf folgenden Code:

  • Die Absicht kann nicht mit den Anmerkungen ausgedrückt werden.
  • Code, der eine Warnung generiert, die aber zur Laufzeit kein echtes Problem darstellt.

Die Warnungen können von UnconditionalSuppressMessageAttribute. Dies ähnelt SuppressMessageAttribute, wird jedoch in der IL beibehalten und in der Kürzungsanalyse berücksichtigt.

Warnung

Wenn Sie Warnungen unterdrücken, müssen Sie basierend auf Invarianten, deren Gültigkeit Sie per Inspektion und Tests überprüft haben, gewährleisten können, dass Ihr Code kürzungskompatibel ist. Verwenden Sie diese Anmerkungen nur mit großer Sorgfalt. Denn wenn sie nicht korrekt sind oder wenn sich Invarianten Ihres Codes ändern, können sie am Ende falschen Code verbergen.

Beispiel:

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

Im vorangegangenen Code wurde die Indexer-Eigenschaft so kommentiert, dass der zurückgegebene Type die Anforderungen von CreateInstance erfüllt. Dadurch wird bereits sichergestellt, dass der Konstruktor TypeWithConstructor beibehalten wird und der Aufruf von CreateInstance keine Warnung auslöst. Zudem wird durch die Indexer-Setter-Anmerkung sichergestellt, dass alle in Type[] gespeicherte Typen über einen Konstruktor verfügen. Die Analyse kann dies jedoch nicht erkennen und erzeugt eine Warnung für den Getter, da sie nicht weiß, dass der zurückgegebene Typ den Konstruktor beibehalten hat.

Wenn Sie sicher sind, dass die Anforderungen erfüllt sind, können Sie diese Warnung unterdrücken, indem Sie dem Getter [UnconditionalSuppressMessage] hinzufügen:

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

Es ist wichtig zu betonen, dass es nur möglich ist, eine Warnung zu unterdrücken, wenn Code oder Anmerkungen vorhanden sind, die sicherstellen, dass die reflektierten Member sichtbare Reflexionsziele sind. Es reicht nicht aus, dass das Element ein Ziel eines Anrufs, Felds oder Eigenschaftenzugriffs war. Es kann vorkommen, dass es manchmal der Fall ist, aber ein solcher Code ist gebunden, um schließlich zu brechen, da weitere Kürzungsoptimierungen hinzugefügt werden. Eigenschaften, Felder und Methoden, die keine sichtbaren Ziele der Spiegelung sind, könnten inlineiert sein, ihre Namen entfernt, in verschiedene Typen verschoben oder anderweitig optimiert werden, um sie zu unterbrechen. Wenn eine Warnung unterdrückt wird, ist es nur zulässig, auf Ziele zu verweisen, die sichtbare Reflexionsziele für das Kürzungsanalysetool waren.

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

Das [DynamicDependency]-Attribut kann verwendet werden, um anzugeben, dass ein Member über eine dynamische Abhängigkeit von anderen Membern verfügt. Dies führt dazu, dass die Member, auf die verwiesen wird, immer dann beibehalten werden, wenn der Member mit dem -Attribut beibehalten wird, aber Warnungen nicht allein stillsetzt. Im Gegensatz zu den anderen Attributen, die der Kürzungsanalyse Informationen über das Reflexionsverhalten des Codes liefern, enthält [DynamicDependency] nur zusätzliche Member. Dies kann zusammen mit [UnconditionalSuppressMessage] verwendet werden, um einige Analysewarnungen zu beheben.

Warnung

Verwenden Sie das [DynamicDependency]-Attribut nur als letzten Ausweg, wenn die anderen Ansätze nicht durchführbar sind. Es ist besser, das Reflexionsverhalten mit [RequiresUnreferencedCode] oder [DynamicallyAccessedMembers] auszudrücken.

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

Ohne DynamicDependency könnte die Kürzung Helper aus MyAssembly oder sogar MyAssembly vollständig entfernen, wenn nicht anderweitig darauf verwiesen wird. Dadurch würde bei der Ausführung eine Warnung in Bezug auf einen möglichen Fehler erzeugt. Das Attribut gewährleistet, dass Helper beibehalten wird.

Das Attribut spezifiziert die zu behaltenden Member über ein string oder über DynamicallyAccessedMemberTypes. Der Typ und die Assembly sind entweder implizit im Attributkontext oder explizit im Attribut angegeben (durch Type oder durch strings für den Typ und den Assemblynamen).

Die Typ- und Member-Zeichenfolgen verwenden eine Variation des Zeichenfolgenformats der Kommentar-ID aus der C#-Dokumentation ohne das Member-Präfix. Die Memberzeichenfolge sollte nicht den Namen des deklarierenden Typs enthalten und kann Parameter weglassen, um alle Member des angegebenen Namens beizubehalten. Einige Beispiele für das Format sind im folgenden Code dargestellt:

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

Das [DynamicDependency]-Attribut ist für Fälle gedacht, in denen eine Methode Reflexionsmuster enthält, die auch mithilfe von DynamicallyAccessedMembersAttribute nicht analysiert werden können.