Introdução aos avisos de corte

Conceitualmente, o corte é simples: ao publicar um aplicativo, o SDK do .NET analisa todo o aplicativo e remove todo o código não utilizado. No entanto, pode ser difícil determinar o que não é utilizado ou, mais precisamente, o que é usado.

Para evitar alterações de comportamento ao cortar aplicativos, o SDK do .NET fornece análise estática da compatibilidade de corte por meio de avisos de corte. O Cortador produz avisos de corte quando encontra código que pode não ser compatível com corte. O código que não é compatível com corte pode produzir alterações comportamentais ou até mesmo falhas em um aplicativo após o corte. O ideal é que todos os aplicativos que usam corte não produzam avisos de corte. Se houver avisos de corte, o aplicativo deverá ser testado completamente após o corte para garantir que não haja alterações de comportamento.

Este artigo ajuda você a entender por que alguns padrões produzem avisos de corte e como esses avisos podem ser resolvidos.

Exemplos de avisos de corte

Para a maioria dos códigos C#, é simples determinar qual código é usado e qual código não é usado. A unidade de corte pode percorrer chamadas de método, referências de campo e de propriedade e assim por diante e determinar qual código é acessado. Infelizmente, alguns recursos, como reflexão, apresentam um problema significativo. Considere o seguinte código:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Neste exemplo, GetType() solicita dinamicamente um tipo com um nome desconhecido e depois imprime os nomes de todos os seus métodos. Como não há como saber no momento da publicação qual nome de tipo será usado, não há como o Cortador saber qual tipo preservará na saída. Talvez esse código tenha funcionado antes do corte (se a existência da entrada era conhecida na estrutura de destino), mas provavelmente isso tenha gerado uma exceção de referência nula após o corte, pois Type.GetType retorna nulo quando o tipo não é encontrado.

Nesse caso, a unidade de corte emite um aviso na chamada para Type.GetType, indicando que ela não pode determinar qual tipo será usado pelo aplicativo.

Como reagir aos avisos de corte

Os avisos de corte se destinam a fornecer previsibilidade ao corte. Há duas grandes categorias de avisos que você provavelmente verá:

  1. A funcionalidade não é compatível com corte
  2. A funcionalidade tem determinados requisitos na entrada para ser compatível com corte

A funcionalidade é incompatível com corte

Normalmente, esses são métodos que não funcionam ou podem ser interrompidos em alguns casos se forem usados em um aplicativo cortado. Um bom exemplo é o método Type.GetType do exemplo anterior. Em um aplicativo cortado, ele pode funcionar, mas não há garantia. Essas APIs são marcadas com RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute é simples e amplo: é um atributo que significa que o membro foi anotado como sendo incompatível com corte. Esse atributo é usado quando o código não é fundamentalmente compatível com corte ou quando a dependência de corte é muito complexa para explicar à unidade de corte. Isso geralmente é verdade para métodos que carregam código dinamicamente, por exemplo, através do LoadFrom(String), enumeram ou pesquisam todos os tipos em um aplicativo ou assembly, por exemplo, através do GetType(), usam a palavra-chave dynamic em C# ou utilizam outras tecnologias de geração de código em runtime. Um exemplo seria:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
    ...
    Assembly.LoadFrom(...);
    ...
}

void TestMethod()
{
    // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
    MethodWithAssemblyLoad();
}

Não há muitas soluções alternativas para RequiresUnreferencedCode. A melhor correção é evitar chamar o método ao cortar e usar outra opção compatível com o corte.

Marcar a funcionalidade como incompatível com corte

Se você estiver escrevendo uma biblioteca e não tiver controle sobre o uso de funcionalidades incompatíveis, você pode marcá-la com RequiresUnreferencedCode. Isso anota seu método como incompatível com corte. O uso de RequiresUnreferencedCode silencia todos os avisos de corte no método especificado, mas produz um aviso sempre que outra pessoa o chamar.

O RequiresUnreferencedCodeAttribute requer que você especifique um Message. A mensagem é mostrada como parte de um aviso relatado ao desenvolvedor que chama o método marcado. Por exemplo:

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

Com o exemplo acima, um aviso para um método específico pode se parecer com isso:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

Os desenvolvedores que chamam essas APIs geralmente não estarão interessados nas particularidades da API afetada ou em detalhes relacionados ao corte.

Uma boa mensagem deve indicar qual funcionalidade não é compatível com corte e, em seguida, orientar o desenvolvedor quais são suas próximas possíveis etapas. Pode sugerir usar uma funcionalidade diferente ou alterar como a funcionalidade é usada. Ela também pode simplesmente afirmar que a funcionalidade ainda não é compatível com corte, sem oferecer uma alternativa clara.

Se as diretrizes para o desenvolvedor se tornarem muito longas para serem incluídas em uma mensagem de aviso, você poderá adicionar um Url para o RequiresUnreferencedCodeAttribute opcional para levar o desenvolvedor a uma página da Web que descreve o problema e possíveis soluções com mais detalhes.

Por exemplo:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

Isso produz um aviso:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method

O uso de RequiresUnreferencedCode geralmente leva a marcar mais métodos com ele, devido ao mesmo motivo. Isso é comum quando um método de alto nível se torna incompatível com corte porque chama um método de baixo nível que não é compatível com corte. Você "propaga" o aviso para uma API pública. Cada uso de RequiresUnreferencedCode precisa de uma mensagem e, nesses casos, as mensagens provavelmente serão as mesmas. Para evitar a duplicação de cadeias de caracteres e facilitar a manutenção, use um campo de cadeia de caracteres constante para armazenar a mensagem:

class Functionality
{
    const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    private void ImplementationOfAssemblyLoading()
    {
        ...
    }

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    public void MethodWithAssemblyLoad()
    {
        ImplementationOfAssemblyLoading();
    }
}

Funcionalidade com requisitos em sua entrada

O corte fornece APIs para especificar requisitos adicionais sobre a entrada para métodos e outros membros que resultam em código compatível com corte. Esses requisitos geralmente são sobre reflexão e a capacidade de acessar determinados membros ou operações em um tipo. Esses requisitos são especificados usando o DynamicallyAccessedMembersAttribute.

Ao contrário de RequiresUnreferencedCode, às vezes, a reflexão pode ser compreendida pela unidade de corte, desde que ela seja anotada corretamente. Vamos dar outra olhada no exemplo original:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

No exemplo anterior, o problema real é Console.ReadLine(). Como qualquer tipo pode ser lido, a unidade de corte não tem como saber se você precisa de métodos em System.DateTime ou System.Guid ou em qualquer outro tipo. Por outro lado, o seguinte código seria bom:

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Aqui, a unidade de corte pode ver o tipo exato que está sendo referenciado: System.DateTime. Agora ela pode usar a análise de fluxo para determinar se precisa manter todos os métodos públicos em System.DateTime. Então, onde DynamicallyAccessMembers entra? Quando a reflexão é dividida entre vários métodos. No código a seguir, podemos ver que o tipo System.DateTime flui para Method3 onde a reflexão é usada para acessar os métodos de System.DateTime,

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

Se você compilar o código anterior, o seguinte aviso será produzido:

IL2070: Program.Method3(Type): o argumento 'this' não satisfaz 'DynamicallyAccessedMemberTypes.PublicMethods' na chamada para 'System.Type.GetMethods()'. O parâmetro 'type' do método 'Method3.Program.Method3(Type)' não tem anotações correspondentes. O valor de origem precisa declarar pelo menos os mesmos requisitos que os declarados no local de destino ao qual foi atribuído.

Visando o desempenho e a estabilidade, a análise de fluxo não é executada entre métodos, portanto, uma anotação é necessária para passar informações entre métodos, desde a chamada de reflexão (GetMethods) até a origem do Type. No exemplo anterior, o aviso de corte diz que GetMethods requer que a instância do objeto Type em que é chamado tenha a anotação PublicMethods, mas a variável type não tem o mesmo requisito. Em outras palavras, precisamos passar os requisitos de GetMethods até o chamador:

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

Depois de anotar o parâmetro type, o aviso original desaparece, mas outro é exibido:

IL2087: o argumento 'type' não atende a 'DynamicallyAccessedMemberTypes.PublicMethods' na chamada para 'Program.Method3(Type)'. O parâmetro genérico 'T' de 'Program.Method2<T>()' não tem anotações correspondentes.

Propagamos anotações até o parâmetro type de Method3, em Method2 temos um problema semelhante. O filtro é capaz de acompanhar o valor T à medida que flui pela chamada para typeof, é atribuído à variável local t e passado para Method3. Nesse ponto, ele vê que o parâmetro type requer PublicMethods, mas não há requisitos em T e produz um novo aviso. Para corrigir isso, devemos "anotar e propagar" aplicando anotações em toda a cadeia de chamadas até chegarmos a um tipo estaticamente conhecido (como System.DateTime ou System.Tuple) ou outro valor anotado. Nesse caso, precisamos anotar o parâmetro de tipo T de Method2.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

Agora não há avisos porque o corte sabe quais membros podem ser acessados por meio de reflexão de runtime (métodos públicos) e em quais tipos (System.DateTime), e os preserva. É uma melhor prática adicionar anotações para que o Cortador saiba o que preservar.

Os avisos produzidos por esses requisitos extras serão suprimidos automaticamente se o código afetado estiver em um método com RequiresUnreferencedCode.

Ao contrário de RequiresUnreferencedCode, que simplesmente relata a incompatibilidade, a adição de DynamicallyAccessedMembers torna o código compatível com corte.

Suprimir avisos do Cortador

Se de alguma forma você puder determinar que a chamada é segura e que todo o código necessário não será cortado, você também poderá suprimir o aviso usando UnconditionalSuppressMessageAttribute. Por exemplo:

[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
    Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
    InitializeEverything();

    MethodWithAssemblyLoad(); // Warning suppressed

    ReportResults();
}

Aviso

Tenha muito cuidado ao suprimir avisos de corte. É possível que a chamada seja compatível com corte agora, mas ao alterar o código que pode ser mudado, você pode esquecer de examinar todas as supressões.

UnconditionalSuppressMessage é como SuppressMessage , mas pode ser visto por publish e outras ferramentas pós-build.

Importante

Não use SuppressMessage ou #pragma warning disable para suprimir avisos do Cortador. Eles só funcionam para o compilador, mas não são preservados no assembly compilado. O Cortador opera em assemblies compilados e não vê a supressão.

A supressão se aplica a todo o corpo do método. Portanto, em nosso exemplo acima, ele suprime todos os avisos IL2026 do método. Isso torna mais difícil de entender, pois não está claro qual método é o problemático, a menos que você adicione um comentário. Além disso, o que é mais importante, se o código mudar no futuro, como no caso de ReportResults se tornar incompatível com corte, nenhuma advertência será relatada para esta chamada de método.

Você pode resolver isso refatorando a chamada de método problemática para um método separado ou função local e, em seguida, aplicando a supressão apenas a esse método:

void TestMethod()
{
    InitializeEverything();

    CallMethodWithAssemblyLoad();

    ReportResults();

    [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
    void CallMethodWithAssemblyLoad()
    {
        MethodWIthAssemblyLoad(); // Warning suppressed
    }
}