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>
comolink
. - 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
PublishTrimmed
o .
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 DynamicBehavior
para . 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 SuppressMessageAttribute
aná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 DynamicDependency
o , 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 string
s 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
.
Comentários
https://aka.ms/ContentUserFeedback.
Brevemente: Ao longo de 2024, vamos descontinuar progressivamente o GitHub Issues como mecanismo de feedback para conteúdos e substituí-lo por um novo sistema de feedback. Para obter mais informações, veja:Submeter e ver comentários