Compartilhar via


Preparar bibliotecas .NET para corte

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

Pré-requisitos

SDK do .NET 6 ou posterior.

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

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

SDK do .NET 7 ou posterior.

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

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

SDK do .NET 8 ou posterior.

Habilitar avisos de corte de biblioteca

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

  • Habilitando o corte específico do projeto usando a propriedade IsTrimmable.
  • Criando um aplicativo de teste de corte que usa a biblioteca e habilitando o corte para o aplicativo de teste. Não é necessário referenciar todas as APIs na biblioteca.

É recomendável usar ambas as abordagens. O corte específico do projeto é conveniente e mostra avisos de corte para um projeto, mas depende das referências que estão sendo 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 o corte específico do projeto

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

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

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

  • É considerado compatível com o corte.
  • Não deve gerar avisos relacionados ao corte ao compilar. Quando usado em um aplicativo cortado, os membros do assembly não utilizados serão cortados na saída final.

A propriedade IsTrimmable usa true como padrão ao configurar uma projeto como compatível com AOT com <IsAotCompatible>true</IsAotCompatible>. Para obter mais informações, confira Analisadores de compatibilidade de AOT.

Para gerar avisos de corte sem marcar o projeto como compatível para 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 aparador 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 cortador 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 cortador precisa para emitir avisos sobre incompatibilidades de corte em:

  • O código da biblioteca.
  • O código que a biblioteca referencia a partir de suas dependências.

Observação

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 dão suporte ao corte. Por exemplo, se a biblioteca usar a 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 for direcionada a um TFM que não possa ser cortado, por exemplo net472 ou netstandard2.0, não haverá nenhum benefício na criação de um aplicativo de teste de corte. O corte só tem suporte para .NET 6 e posterior.

  • Defina <TrimmerDefaultAction> como link.
  • Adicione <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto da biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz do cortador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que todas as partes da biblioteca sejam analisadas. Ele informa ao aparador que esse assembly é uma "raiz". Um assembly "raiz" significa que o cortador analisa cada chamada na biblioteca e percorre todos os caminhos de código originados desse assembly.
  • Adicione <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto da biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz do cortador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que todas as partes da biblioteca sejam analisadas. Ele informa ao aparador que esse assembly é uma "raiz". Um assembly "raiz" significa que o cortador analisa cada chamada na biblioteca e percorre todos os caminhos de código originados desse assembly.
  • Adicione <PublishTrimmed>true</PublishTrimmed>.
  • Adicione uma referência ao projeto da biblioteca com <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique a biblioteca como um assembly raiz do cortador com <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garante que todas as partes da biblioteca sejam analisadas. Ele informa ao aparador que esse assembly é uma "raiz". Um assembly "raiz" significa que o cortador analisa cada chamada na biblioteca e percorre todos os caminhos de código originados 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>

Observação: 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 runtime (RID) de destino.

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 por vez, adicione todas ao mesmo projeto como ProjectReference e itens TrimmerRootAssembly. Adicionar todas as bibliotecas ao mesmo projeto com os itens ProjectReference e TrimmerRootAssembly alertará sobre as dependências se qualquer das bibliotecas raiz usar uma API de corte não amigável em uma dependência. Para ver avisos que têm a ver apenas com uma biblioteca específica, faça referência somente a essa biblioteca.

Observação: Os resultados da análise dependem dos detalhes de implementação das dependências. Atualizar para uma nova versão de uma dependência pode resultar em 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 interruptiva quando a biblioteca é usada com PublishTrimmed.

Resolver avisos de corte

As etapas anteriores geram avisos sobre o código que podem causar problemas quando usados 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, por meio 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 anterior realçado indica que a biblioteca chama um método que foi explicitamente anotado como incompatível com o corte. Para se livrar do aviso, verifique se MyMethod precisa chamar DynamicBehavior. Nesse caso, anote o chamador MyMethod com [RequiresUnreferencedCode] que propaga o aviso para que os chamadores de MyMethod obtenham 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 chamarão a biblioteca:

  • Obtenha avisos apenas para métodos públicos que não podem ser cortados.
  • Não receba avisos como 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())
        {
            // ...
        }
    }
}

No código anterior, UseMethods está chamando um método de reflexão que tem um requisito [DynamicallyAccessedMembers]. O requisito indica que os métodos públicos do tipo estão disponíveis. Atenda ao 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 produzem avisos se passarem valores que não atendam ao requisito PublicMethods. Semelhante a [RequiresUnreferencedCode], depois de propagar esses avisos para as APIs públicas, estará pronto.

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

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

Recomendações

  • Evite a reflexão quando possível. Ao usar a reflexão, minimize o escopo de reflexão para que ele seja acessível apenas 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 pode ser anotado com DynamicallyAccessedMembers
  • Quando o código for incompatível com o corte, anote-o com RequiresUnreferencedCode e propague essa anotação aos chamadores até que as APIs públicas relevantes sejam anotadas.
  • Evite usar o código que usa a reflexão de uma maneira 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. Anotar métodos virtuais ou de interface requer que todas as substituições tenham anotações correspondentes.
  • Se uma API for amplamente incompatível com o corte, as abordagens alternativas de codificação para a API poderão ser consideradas. Um exemplo comum são serializadores baseados em reflexão. Nesses casos, considere a adoção de outras tecnologias, como geradores de origem, para produzir código que seja analisado estaticamente com mais facilidade. Por exemplo, confira Como usar a geração de origem no 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 UnconditionalSuppressMessageAttribute suprimidos. Isso é semelhante a SuppressMessageAttribute, mas é persistente em IL e respeitado durante a análise de corte.

Aviso

Ao suprimir avisos, você é responsável por garantir a compatibilidade de corte do código com base em invariáveis que você sabe que são verdadeiros por inspeção e teste. Tenha cuidado com essas anotações, pois se elas estiverem incorretas ou se as invariáveis de seu código forem alteradas, elas poderão acabar ocultando 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 do indexador foi anotada para que o Type retornado atenda aos requisitos de CreateInstance. Isso garante que o construtor TypeWithConstructor seja mantido e que a chamada para CreateInstance não gere um aviso. A anotação do setter do indexador garante que todos os tipos armazenados no Type[] tenham um construtor. No entanto, a análise não consegue ver isso e gera um aviso para o getter, pois não sabe que o tipo retornado preservou seu construtor.

Se você tem certeza de que os requisitos foram atendidos, poderá 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 destacar que só será 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 o destino de uma chamada, campo ou acesso à propriedade. Pode parecer ser o caso às vezes, mas esse código é obrigado a quebrar eventualmente conforme mais otimizações de corte são adicionadas. As propriedades, campos e métodos que não são destinos visíveis de reflexão podem ser embutidos, ter seus nomes removidos, ser movidos para diferentes tipos ou otimizados de outra forma para interromper a reflexão. Ao suprimir um aviso, só será permitido refletir sobre destinos 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();
}

DynamicDependency

O atributo [DynamicDependency] pode ser usado para indicar que um membro tem uma dependência dinâmica em outros membros. Isso faz com que os membros referenciados sejam mantidos sempre que o membro com o atributo é mantido, mas não silencia 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 junto com [UnconditionalSuppressMessage] para corrigir alguns avisos de análise.

Aviso

Use o atributo [DynamicDependency] somente 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 DynamicDependency, o corte pode remover Helper de MyAssembly ou remover MyAssembly completamente se não for referenciado 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 por meio de um string ou DynamicallyAccessedMemberTypes. O tipo e o assembly são implícitos no contexto do atributo ou explicitamente especificados no atributo (por Type, ou por strings para o tipo e 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 C#, sem o prefixo do membro. A cadeia de caracteres de membro não deve incluir o nome do tipo declarativo 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 atributo [DynamicDependency] foi criado 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.