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:
- Descreve como preparar bibliotecas para corte.
- Fornece recomendações para resolver avisos comuns de corte.
Pré-requisitos
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.
- 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>
</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. 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 interruptiva quando a biblioteca é usada com
PublishTrimmed
.
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, 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 praticamente incompatível com cortes, talvez seja necessário considerar abordagens alternativas de codificação para a API. 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 suprimidos por UnconditionalSuppressMessageAttribute. 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 é suficiente 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 ser otimizados de maneiras que interrompam a reflexão sobre eles. 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 string
s 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 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 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
.