Příprava knihoven .NET na oříznutí

Sada .NET SDK umožňuje zmenšit velikost samostatných aplikací oříznutím. Oříznutí odebere z aplikace nepoužitý kód a jeho závislosti. Ne všechny kódy jsou kompatibilní s oříznutím. .NET poskytuje upozornění analýzy oříznutí k detekci vzorů, které můžou přerušit oříznuté aplikace. Tento článek:

Požadavky

Sada .NET 6 SDK nebo novější

Pokud chcete získat nejaktuálnější upozornění a pokrytí analyzátoru oříznutí:

  • Nainstalujte a použijte sadu .NET 8 SDK nebo novější.
  • Cíl net8.0 nebo novější.

Sada .NET 7 SDK nebo novější

Pokud chcete získat nejaktuálnější upozornění a pokrytí analyzátoru oříznutí:

  • Nainstalujte a použijte sadu .NET 8 SDK nebo novější.
  • Cíl net8.0 nebo novější.

Sada .NET 8 SDK nebo novější

Povolení upozornění oříznutí knihovny

Upozornění oříznutí v knihovně najdete pomocí některé z následujících metod:

  • Povolení oříznutí specifického IsTrimmable pro projekt pomocí vlastnosti
  • Vytvoření testovací aplikace pro oříznutí, která používá knihovnu a povolení oříznutí pro testovací aplikaci Není nutné odkazovat na všechna rozhraní API v knihovně.

Doporučujeme používat oba přístupy. Oříznutí specifické pro projekt je pohodlné a zobrazuje upozornění oříznutí pro jeden projekt, ale spoléhá na odkazy, které jsou označené jako kompatibilní s oříznutím, aby se zobrazila všechna upozornění. Oříznutí testovací aplikace je více funkční, ale zobrazí se všechna upozornění.

Povolení oříznutí specifického pro projekt

Nastavte <IsTrimmable>true</IsTrimmable> v souboru projektu.

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

Nastavením vlastnosti IsTrimmable MSBuild označí true sestavení jako "trimmable" a povolí upozornění oříznutí. "Trimmable" znamená projekt:

  • Považuje se za kompatibilní s oříznutím.
  • Při vytváření by se nemělo generovat upozornění související s oříznutím. Při použití v oříznuté aplikaci má sestavení jeho nepoužívané členy oříznuté v konečném výstupu.

Vlastnost IsTrimmable je výchozí true při konfiguraci projektu jako AOT kompatibilní s <IsAotCompatible>true</IsAotCompatible>. Další informace najdete v tématu Analyzátory kompatibility AOT.

Chcete-li generovat upozornění oříznutí bez označení projektu jako kompatibilní s oříznutím, použijte <EnableTrimAnalyzer>true</EnableTrimAnalyzer> místo <IsTrimmable>true</IsTrimmable>.

Zobrazení všech upozornění pomocí testovací aplikace

Aby se zobrazila všechna upozornění analýzy pro knihovnu, musí se při zatřižování analyzovat implementace knihovny a všech závislostí, které knihovna používá.

Při sestavování a publikování knihovny:

  • Implementace závislostí nejsou k dispozici.
  • Dostupná referenční sestavení nemají dostatek informací pro oříznutí, aby bylo možné určit, jestli jsou kompatibilní s oříznutím.

Kvůli omezením závislostí se musí vytvořit samostatná testovací aplikace, která používá knihovnu a její závislosti. Testovací aplikace obsahuje všechny informace, které zastřihovač vyžaduje k vydání upozornění na nekompatibilitu oříznutí v:

  • Kód knihovny.
  • Kód, na který knihovna odkazuje ze závislostí.

Poznámka:

Pokud má knihovna jiné chování v závislosti na cílovém rozhraní, vytvořte testovací aplikaci pro oříznutí pro každou z cílových architektur, která podporuje oříznutí. Pokud například knihovna používá podmíněnou kompilaci , například #if NET7_0 ke změně chování.

Vytvoření testovací aplikace pro oříznutí:

  • Vytvořte samostatný projekt konzolové aplikace.
  • Přidejte odkaz na knihovnu.
  • Upravte projekt podobný následujícímu projektu pomocí následujícího seznamu:

Pokud knihovna cílí na TFM, který není možné oříznout, nebo net472netstandard2.0neexistuje žádný přínos pro vytvoření testovací aplikace pro oříznutí. Oříznutí se podporuje jenom pro .NET 6 a novější.

  • Nastavte <TrimmerDefaultAction> na hodnotu link.
  • Přidat <PublishTrimmed>true</PublishTrimmed>.
  • Přidejte odkaz na projekt knihovny pomocí <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.
  • Přidat <PublishTrimmed>true</PublishTrimmed>.
  • Přidejte odkaz na projekt knihovny pomocí <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.
  • Přidat <PublishTrimmed>true</PublishTrimmed>.
  • Přidejte odkaz na projekt knihovny pomocí <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.

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

Poznámka: V předchozím souboru projektu při použití .NET 7 nahraďte <TargetFramework>net8.0</TargetFramework> .<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>

Po aktualizaci souboru projektu spusťte dotnet publish identifikátor cílovou modul runtime (RID).

dotnet publish -c Release -r <RID>

Postupujte podle předchozího vzoru pro více knihoven. Pokud chcete zobrazit upozornění analýzy oříznutí pro více než jednu knihovnu najednou, přidejte je všechny do stejného projektu jako ProjectReference položky a TrimmerRootAssembly položky. Přidání všech knihoven do stejného projektu a ProjectReferenceTrimmerRootAssembly položek upozorní na závislosti, pokud některá z kořenových knihoven používá v závislosti rozhraní API pro oříznutí. Pokud chcete zobrazit upozornění, která je potřeba udělat jenom s konkrétní knihovnou, odkazujte pouze na tuto knihovnu.

Poznámka: Výsledky analýzy závisí na podrobnostech implementace závislostí. Aktualizace na novou verzi závislosti může zavádět upozornění analýzy:

  • Pokud nová verze přidala nepochopené vzory reflexe.
  • I když nedošlo k žádným změnám rozhraní API.
  • Představujeme upozornění analýzy oříznutí je zásadní změna při použití knihovny s PublishTrimmed.

Řešení upozornění oříznutí

Předchozí kroky generují upozornění na kód, který může způsobovat problémy při použití v oříznuté aplikaci. Následující příklady ukazují nejběžnější upozornění s doporučeními pro jejich opravu.

RequiresUnreferencedCode

Vezměte v úvahu následující kód, který používá [RequiresUnreferencedCode] k označení, že zadaná metoda vyžaduje dynamický přístup k kódu, který není odkazován staticky, například prostřednictvím 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()
    {
    }
}

Předchozí zvýrazněný kód označuje knihovnu volání metody, která byla explicitně označena jako nekompatibilní s oříznutím. Chcete-li se zbavit upozornění, zvažte, zda MyMethod je třeba zavolat DynamicBehavior. Pokud ano, označte volajícího MyMethod poznámkami [RequiresUnreferencedCode] , se kterým se upozornění rozšíří, aby volajícím MyMethod místo toho zobrazilo upozornění:

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

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

Po rozšíření atributu až do veřejného rozhraní API aplikace volají knihovnu:

  • Zobrazí se upozornění jenom pro veřejné metody, které se nedají oříznout.
  • Nezobrazují se upozornění jako IL2104: Assembly 'MyLibrary' produced trim warnings.

DynamickyAccessedMembers

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())
        {
            // ...
        }
    }
}

V předchozím kódu volá metodu reflexe, UseMethods která má [DynamicallyAccessedMembers] požadavek. Požadavek uvádí, že veřejné metody typu jsou k dispozici. Splnění požadavku přidáním stejného požadavku na parametr .UseMethods

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

Když teď všechna volání UseMethods vygenerují upozornění, předávají hodnoty, které nevyhovují PublicMethods požadavkům. [RequiresUnreferencedCode]Podobně jako v případě, že po rozšíření těchto upozornění do veřejných rozhraní API budete hotovi.

V následujícím příkladu neznámý typ proudí do anotovaného parametru metody. Neznámé Type je z pole:

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

Podobně zde problém spočívá v tom, že pole type se předává do parametru s těmito požadavky. Opravili jsme ho přidáním [DynamicallyAccessedMembers] do pole. [DynamicallyAccessedMembers] varuje o kódu, který přiřadí nekompatibilní hodnoty k poli. Někdy tento proces pokračuje, dokud nebude veřejné rozhraní API opatřeno poznámkami, a jindy skončí, když konkrétní typ přejde do umístění s těmito požadavky. Příklad:

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

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

V tomto případě analýza oříznutí uchovává veřejné metody Tuplea vytváří další upozornění.

Doporučení

  • Pokud je to možné, vyhněte se reflexi. Při použití reflexe minimalizujte rozsah reflexe tak, aby byl dostupný jenom z malé části knihovny.
  • Pokud je to možné, přiřazte kód staticky DynamicallyAccessedMembers a vyjádřete požadavky na oříznutí.
  • Zvažte přeuspořádání kódu, aby postupoval podle analyzovatelného vzoru, který může být opatřen poznámkami DynamicallyAccessedMembers
  • Pokud kód není kompatibilní s oříznutím, označte ho poznámkami RequiresUnreferencedCode a rozšíříte tuto poznámku volajícím, dokud nebudou příslušná veřejná rozhraní API opatřena poznámkami.
  • Nepoužívejte kód, který používá reflexi způsobem, kterému statická analýza nerozumí. Například reflexe statických konstruktorů by se měla vyhnout. Použití staticky neanalyzovatelné reflexe ve statických konstruktorech vede k šíření upozornění na všechny členy třídy.
  • Vyhněte se přidávání poznámek k virtuálním metodám nebo metodám rozhraní. Přidávání poznámek k virtuálním metodám nebo metod rozhraní vyžaduje, aby všechny přepsání měly odpovídající poznámky.
  • Pokud je rozhraní API většinou nekompatibilní, možná bude potřeba zvážit alternativní přístupy k kódování rozhraní API. Běžným příkladem jsou serializátory založené na reflexi. V těchto případech zvažte přijetí dalších technologií, jako jsou generátory zdrojů, k vytvoření kódu, který je snadněji staticky analyzován. Podívejte se například, jak používat generování zdrojového kódu v souboru System.Text.Json.

Řešení upozornění pro ne analyzovatelné vzory

Upozornění je lepší vyřešit vyjádřením záměru kódu a [RequiresUnreferencedCode]DynamicallyAccessedMembers v případě, že je to možné. V některých případech vás ale může zajímat povolení oříznutí knihovny, která používá vzory, které nelze vyjádřit pomocí těchto atributů nebo bez refaktoringu existujícího kódu. Tato část popisuje některé pokročilé způsoby řešení upozornění analýzy oříznutí.

Upozorňující

Tyto techniky můžou změnit chování nebo kód nebo vést k nesprávným výjimkám doby běhu.

Nepodmíněné zprávy

Vezměte v úvahu kód, který:

  • Záměr nelze vyjádřit pomocí poznámek.
  • Vygeneruje upozornění, ale nepředstavuje skutečný problém za běhu.

Upozornění lze potlačit UnconditionalSuppressMessageAttribute. To se podobá SuppressMessageAttribute, ale to je trvalé v IL a respektovat během analýzy oříznutí.

Upozorňující

Při potlačení upozornění zodpovídáte za záruku kompatibility oříznutí kódu na základě invariantů, o kterých víte, že jsou pravdivé kontrolou a testováním. U těchto poznámek buďte opatrní, protože pokud jsou nesprávné nebo pokud jsou invarianty změny kódu, můžou nakonec skrýt nesprávný kód.

Příklad:

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

V předchozím kódu byla vlastnost indexeru anotována tak, aby vrácená Type hodnota splňovala požadavky .CreateInstance Tím se zajistí, že TypeWithConstructor je konstruktor zachován a že volání CreateInstance se nevaruje. Poznámka k setter indexeru zajišťuje, že všechny typy uložené v konstruktoru Type[] . Analýza ale tuto možnost nevidí a vygeneruje upozornění pro getter, protože neví, že vrácený typ má svůj konstruktor zachován.

Pokud jste si jistí, že jsou splněné požadavky, můžete toto upozornění mlčet přidáním [UnconditionalSuppressMessage] do getteru:

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

Je důležité podtrhnout, že je platné potlačit upozornění pouze v případě, že existují poznámky nebo kód, které zajistí, aby odrazové členy byly viditelné cíle reflexe. Nestačí, aby byl člen cílem přístupu k volání, poli nebo vlastnosti. Může se zdát, že se jedná o případ, ale takový kód je vázán k přerušení nakonec, protože se přidají další optimalizace oříznutí. Vlastnosti, pole a metody, které nejsou viditelné cíle reflexe, mohou být vloženy, mají jejich názvy odebrány, přesunout se do různých typů nebo jinak optimalizovat způsobem, který se na nich odráží. Když potlačíte upozornění, je možné odrážet pouze cíle, které byly viditelné cíle odrazu, na jiném místě analyzátoru oříznutí.

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

Atribut [DynamicDependency] lze použít k označení, že člen má dynamickou závislost na jiných členech. Výsledkem je zachování odkazovaných členů vždy, když je člen s atributem zachován, ale neumlčuje upozornění sama. Na rozdíl od ostatních atributů, které informují analýzu oříznutí o chování reflexe kódu, [DynamicDependency] udržuje pouze ostatní členy. Můžete ho použít společně s [UnconditionalSuppressMessage] opravou některých upozornění analýzy.

Upozorňující

Atribut používejte [DynamicDependency] pouze jako poslední možnost, pokud ostatní přístupy nejsou přijatelné. Je vhodnější vyjádřit chování reflexe pomocí [RequiresUnreferencedCode] nebo [DynamicallyAccessedMembers].

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

Bez DynamicDependencyořezávání se může odebrat nebo úplně odebrat MyAssemblyHelperMyAssembly, pokud není odkazováno jinde, což vygeneruje upozornění, které značí možné selhání v době běhu. Atribut zajišťuje, že Helper se zachová.

Atribut určuje členy, které mají být zachovány prostřednictvím nebo string prostřednictvím DynamicallyAccessedMemberTypes. Typ a sestavení jsou buď implicitní v kontextu atributu, nebo explicitně zadané v atributu (podle Type, nebo podle strings pro typ a název sestavení).

Typ a členské řetězce používají variantu formátu řetězce ID komentáře dokumentace jazyka C# bez předpony člena. Řetězec člena by neměl obsahovat název deklarujícího typu a může vynechat parametry pro zachování všech členů zadaného názvu. Některé příklady formátu jsou uvedeny v následujícím kódu:

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

Atribut [DynamicDependency] je navržen tak, aby byl použit v případech, kdy metoda obsahuje vzory reflexe, které nelze analyzovat ani s pomocí DynamicallyAccessedMembersAttribute.