Partilhar via


Preparar bibliotecas .NET para corte

O SDK do .NET torna possível reduzir o tamanho de aplicativos autônomos cortando. O corte remove o código não utilizado do aplicativo e suas dependências. Nem todo o código é compatível com o corte. O .NET fornece avisos de análise de corte para detetar padrões que podem quebrar aplicativos cortados. Este artigo:

  • Descreve como preparar bibliotecas para corte.
  • Fornece recomendações para resolver avisos de corte comuns.

Pré-requisitos

SDK do .NET 6 ou posterior.

Para obter os avisos de corte e a cobertura do analisador mais atualizados:

  • Instale e use o SDK do .NET 8 ou posterior.
  • Alvo net8.0 ou posterior.

SDK do .NET 7 ou posterior.

Para obter os avisos de corte e a cobertura do analisador mais atualizados:

  • Instale e use o SDK do .NET 8 ou posterior.
  • Alvo net8.0 ou posterior.

SDK do .NET 8 ou posterior.

Ativar avisos de corte de biblioteca

Os avisos de corte em uma biblioteca podem ser encontrados com um dos seguintes métodos:

  • Habilitando o corte específico do projeto usando a IsTrimmable propriedade.
  • Criar um aplicativo de teste de corte que usa a biblioteca e habilitar o corte para o aplicativo de teste. Não é necessário fazer referência a todas as APIs na biblioteca.

Recomendamos o uso de ambas as abordagens. O corte específico do projeto é conveniente e mostra avisos de corte para um projeto, mas depende das referências marcadas como compatíveis com o corte para ver todos os avisos. Cortar um aplicativo de teste é mais trabalhoso, mas mostra todos os avisos.

Habilitar corte específico do projeto

Definido <IsTrimmable>true</IsTrimmable> no arquivo de projeto.

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

Definir a propriedade IsTrimmable MSBuild para true marcar o assembly como "trimmable" e habilita avisos de corte. "Trimmable", o projeto:

  • É considerado compatível com o corte.
  • Não deve gerar avisos relacionados ao acabamento durante a construção. Quando usado em um aplicativo cortado, o assembly tem seus membros não utilizados cortados na saída final.

O IsTrimmable padrão da propriedade é ao true configurar um projeto como compatível com AOT com <IsAotCompatible>true</IsAotCompatible>. Para obter mais informações, consulte Analisadores de compatibilidade AOT.

Para gerar avisos de corte sem marcar o projeto como compatível com corte, use <EnableTrimAnalyzer>true</EnableTrimAnalyzer> em vez de <IsTrimmable>true</IsTrimmable>.

Mostrar todos os avisos com o aplicativo de teste

Para mostrar todos os avisos de análise para uma biblioteca, o trimmer deve analisar a implementação da biblioteca e de todas as dependências que a biblioteca usa.

Ao criar e publicar uma biblioteca:

  • As implementações das dependências não estão disponíveis.
  • Os assemblies de referência disponíveis não têm informações suficientes para o aparador determinar se são compatíveis com o corte.

Devido às limitações de dependência, um aplicativo de teste autônomo que usa a biblioteca e suas dependências deve ser criado. O aplicativo de teste inclui todas as informações que o aparador requer para emitir um aviso sobre incompatibilidades de corte em:

  • O código da biblioteca.
  • O código ao qual a biblioteca faz referência a partir de suas dependências.

Nota

Se a biblioteca tiver um comportamento diferente dependendo da estrutura de destino, crie um aplicativo de teste de corte para cada uma das estruturas de destino que oferecem suporte a corte. Por exemplo, se a biblioteca usa compilação condicional , como #if NET7_0 para alterar o comportamento.

Para criar o aplicativo de teste de corte:

  • Crie um projeto de aplicativo de console separado.
  • Adicione uma referência à biblioteca.
  • Modifique o projeto semelhante ao projeto mostrado abaixo usando a seguinte lista:

Se a biblioteca tiver como alvo um TFM que não é trimmable, por exemplo net472 ou netstandard2.0, não há nenhum benefício em criar um aplicativo de teste de corte. O corte só é suportado para .NET 6 e posterior.

  • Defina <TrimmerDefaultAction> como link.
  • Adicionar <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto de biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz de aparador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que cada parte da biblioteca seja analisada. Diz ao aparador que esta montagem é uma "raiz". Um assembly "raiz" significa que o trimmer analisa cada chamada na biblioteca e percorre todos os caminhos de código que se originam desse assembly.
  • Adicionar <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto de biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz de aparador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que cada parte da biblioteca seja analisada. Diz ao aparador que esta montagem é uma "raiz". Um assembly "raiz" significa que o trimmer analisa cada chamada na biblioteca e percorre todos os caminhos de código que se originam desse assembly.
  • Adicionar <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto de biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz de aparador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que cada parte da biblioteca seja analisada. Diz ao aparador que esta montagem é uma "raiz". Um assembly "raiz" significa que o trimmer analisa cada chamada na biblioteca e percorre todos os caminhos de código que se originam desse assembly.

Arquivo .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: No arquivo de projeto anterior, ao usar o .NET 7, substitua <TargetFramework>net8.0</TargetFramework> por <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>

Depois que o arquivo de projeto for atualizado, execute dotnet publish com o identificador de tempo de execução de destino (RID).

dotnet publish -c Release -r <RID>

Siga o padrão anterior para várias bibliotecas. Para ver avisos de análise de corte para mais de uma biblioteca de cada vez, adicione-os todos ao mesmo projeto como ProjectReference e TrimmerRootAssembly itens. Adicionar todas as bibliotecas ao mesmo projeto com ProjectReference e TrimmerRootAssembly itens avisa sobre dependências se qualquer uma das bibliotecas raiz usar uma API de corte hostil em uma dependência. Para ver avisos que têm a ver apenas com uma biblioteca específica, consulte apenas essa biblioteca.

Nota: Os resultados da análise dependem dos detalhes de implementação das dependências. A atualização para uma nova versão de uma dependência pode introduzir avisos de análise:

  • Se a nova versão adicionou padrões de reflexão não compreendidos.
  • Mesmo que não houvesse alterações na API.
  • A introdução de avisos de análise de corte é uma alteração importante quando a biblioteca é usada com PublishTrimmedo .

Resolver avisos de corte

As etapas anteriores produzem avisos sobre o código que pode causar problemas quando usado em um aplicativo cortado. Os exemplos a seguir mostram os avisos mais comuns com recomendações para corrigi-los.

RequiresUnreferencedCode

Considere o código a seguir que usa [RequiresUnreferencedCode] para indicar que o método especificado requer acesso dinâmico ao código que não é referenciado estaticamente, por exemplo, através de 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()
    {
    }
}

O código realçado anterior indica que a biblioteca chama um método que foi explicitamente anotado como incompatível com o corte. Para se livrar do aviso, considere se MyMethod precisa ligar DynamicBehaviorpara . Em caso afirmativo, anote o chamador MyMethod com [RequiresUnreferencedCode] o qual propaga o aviso para que os chamadores recebam MyMethod um aviso:

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

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

Depois de propagar o atributo até a API pública, os aplicativos chamam a biblioteca:

  • Receba avisos apenas para métodos públicos que não são trimmable.
  • Não receba avisos como IL2104: Assembly 'MyLibrary' produced trim warnings.

Membros DynamicallyAccessed.

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

No código anterior, UseMethods está chamando um método de reflexão que tem um [DynamicallyAccessedMembers] requisito. O requisito estabelece que os métodos públicos do tipo estão disponíveis. Satisfazer o requisito adicionando o mesmo requisito ao parâmetro de UseMethods.

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

Agora, todas as chamadas para UseMethods produzir avisos se passarem em valores que não satisfaçam o PublicMethods requisito. Semelhante ao [RequiresUnreferencedCode], depois de propagar esses avisos para APIs públicas, você está pronto.

No exemplo a seguir, um Type desconhecido flui para o parâmetro de método anotado. O desconhecido Type é de um 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);
}

Da mesma forma, aqui o problema é que o campo type é passado para um parâmetro com esses requisitos. É corrigido adicionando [DynamicallyAccessedMembers] ao campo. [DynamicallyAccessedMembers] avisa sobre o código que atribui valores incompatíveis ao campo. Às vezes, esse processo continua até que uma API pública seja anotada, e outras vezes termina quando um tipo concreto flui para um local com esses requisitos. Por exemplo:

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

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

Neste caso, a análise de corte mantém métodos públicos de Tuple, e produz avisos adicionais.

Recomendações

  • Evite a reflexão sempre que possível. Ao usar a reflexão, minimize o escopo da reflexão para que ela seja acessível apenas a partir de uma pequena parte da biblioteca.
  • Anote o código com DynamicallyAccessedMembers para expressar estaticamente os requisitos de corte quando possível.
  • Considere reorganizar o código para fazê-lo seguir um padrão analisável que possa ser anotado com DynamicallyAccessedMembers
  • Quando o código for incompatível com o corte, anote-o e RequiresUnreferencedCode propague essa anotação para chamadores até que as APIs públicas relevantes sejam anotadas.
  • Evite usar código que usa reflexão de uma forma não compreendida pela análise estática. Por exemplo, a reflexão em construtores estáticos deve ser evitada. O uso de reflexão estaticamente não analisável em construtores estáticos resulta na propagação do aviso para todos os membros da classe.
  • Evite anotar métodos virtuais ou métodos de interface. A anotação de métodos virtuais ou de interface requer que todas as substituições tenham anotações correspondentes.
  • Se uma API for praticamente incompatível, talvez seja necessário considerar abordagens de codificação alternativas à API. Um exemplo comum são serializadores baseados em reflexão. Nesses casos, considere adotar outras tecnologias, como geradores de código-fonte, para produzir código que seja mais facilmente analisado estaticamente. Por exemplo, consulte Como usar a geração de código-fonte em System.Text.Json

Resolver avisos para padrões não analisáveis

É melhor resolver avisos expressando a intenção do seu código usando [RequiresUnreferencedCode] e DynamicallyAccessedMembers quando possível. No entanto, em alguns casos, você pode estar interessado em habilitar o corte de uma biblioteca que usa padrões que não podem ser expressos com esses atributos, ou sem refatorar o código existente. Esta seção descreve algumas maneiras avançadas de resolver avisos de análise de corte.

Aviso

Essas técnicas podem alterar o comportamento ou seu código ou resultar em exceções de tempo de execução se usadas incorretamente.

UnconditionalSuppressMessage

Considere o código que:

  • A intenção não pode ser expressa com as anotações.
  • Gera um aviso, mas não representa um problema real em tempo de execução.

Os avisos podem ser suprimidos UnconditionalSuppressMessageAttribute. Isso é semelhante ao , mas persiste na IL e é respeitado durante a SuppressMessageAttributeanálise de corte.

Aviso

Ao suprimir avisos, você é responsável por garantir a compatibilidade de corte do código com base em invariantes que você sabe serem verdadeiras por inspeção e testes. Tenha cuidado com essas anotações, porque se elas estiverem incorretas, ou se invariantes do seu código forem alteradas, elas podem acabar escondendo o código incorreto.

Por exemplo:

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

No código anterior, a propriedade indexer foi anotada para que o retornado Type atenda aos requisitos de CreateInstance. Isso garante que o TypeWithConstructor construtor seja mantido e que a chamada para CreateInstance não avise. A anotação setter do Type[] indexador garante que todos os tipos armazenados no tenham um construtor. No entanto, a análise não é capaz de ver isso e produz um aviso para o getter, porque ele não sabe que o tipo retornado tem seu construtor preservado.

Se tiver certeza de que os requisitos foram atendidos, você pode silenciar este aviso adicionando [UnconditionalSuppressMessage] ao 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 sublinhar que só é válido suprimir um aviso se houver anotações ou código que garantam que os membros refletidos sejam alvos visíveis de reflexão. Não basta que o membro tenha sido alvo de uma chamada, campo ou acesso à propriedade. Pode parecer ser o caso às vezes, mas esse código está fadado a quebrar eventualmente à medida que mais otimizações de corte são adicionadas. Propriedades, campos e métodos que não são alvos visíveis de reflexão podem ser embutidos, ter seus nomes removidos, ser movidos para tipos diferentes ou otimizados de forma a quebrar a reflexão sobre eles. Ao suprimir um aviso, só é permitido refletir sobre alvos que eram alvos visíveis de reflexão para o analisador de corte em outro lugar.

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

Dependência Dinâmica

O [DynamicDependency] atributo pode ser usado para indicar que um membro tem uma dependência dinâmica de outros membros. Isso faz com que os membros referenciados sejam mantidos sempre que o membro com o atributo é mantido, mas não silencia os avisos por conta própria. Ao contrário dos outros atributos, que informam a análise de corte sobre o comportamento de reflexão do código, [DynamicDependency] mantém apenas outros membros. Isso pode ser usado em conjunto com [UnconditionalSuppressMessage] para corrigir alguns avisos de análise.

Aviso

Use [DynamicDependency] o atributo apenas como último recurso quando as outras abordagens não forem viáveis. É preferível expressar o comportamento de reflexão usando [RequiresUnreferencedCode] ou [DynamicallyAccessedMembers].

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

Sem DynamicDependencyo , o corte pode ser removido Helper ou removido MyAssembly completamente se não for referenciado MyAssembly em outro lugar, produzindo um aviso que indica uma possível falha em tempo de execução. O atributo garante que Helper seja mantido.

O atributo especifica os membros a serem mantidos via string a ou via DynamicallyAccessedMemberTypes. O tipo e o assembly estão implícitos no contexto do atributo ou explicitamente especificados no atributo (por Type, ou por strings para o tipo e o nome do assembly).

As cadeias de caracteres de tipo e membro usam uma variação do formato de cadeia de caracteres de ID de comentário da documentação do C#, sem o prefixo do membro. A cadeia de caracteres de membro não deve incluir o nome do tipo de declaração e pode omitir parâmetros para manter todos os membros do nome especificado. Alguns exemplos do formato são mostrados no código a seguir:

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

O [DynamicDependency] atributo é projetado para ser usado em casos em que um método contém padrões de reflexão que não podem ser analisados mesmo com a ajuda de DynamicallyAccessedMembersAttribute.