Introducción a las advertencias de recorte

Conceptualmente, el recorte es sencillo: al publicar una aplicación, el SDK de .NET analiza toda la aplicación y quita todo el código no utilizado. Sin embargo, puede ser difícil determinar qué es lo que no se usa o, más concretamente, lo que se usa.

Para evitar cambios de comportamiento al recortar aplicaciones, el SDK de .NET proporciona un análisis estático de la compatibilidad de recorte mediante advertencias de recorte. El recortador genera advertencias de recorte cuando encuentra código que puede no ser compatible con el recorte. El código que no es compatible con el recorte puede producir cambios de comportamiento, o incluso bloqueos, en una aplicación después de que se haya recortado. Lo ideal es que todas las aplicaciones que usan el recorte no generen advertencias de recorte. Si hay advertencias de recorte, la aplicación debe probarse exhaustivamente después del recorte para asegurarse de que no hay cambios de comportamiento.

Este artículo le ayudará a comprender por qué algunos patrones generan advertencias de recorte y cómo se pueden abordar estas advertencias.

Ejemplos de advertencias de recorte

Para la mayoría del código de C#, es sencillo determinar qué código se usa y qué código no se usa. El recortador puede recorrer las llamadas de métodos, las referencias a campos y propiedades, etc., y determinar a qué código se accede. Desafortunadamente, algunas características, como la reflexión, presentan un problema importante. Observe el código siguiente:

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

En este ejemplo, GetType() solicita dinámicamente un tipo con un nombre desconocido y, a continuación, imprime los nombres de todos sus métodos. Dado que no hay ninguna manera de saber en tiempo de publicación qué nombre de tipo se va a usar, no hay ninguna manera de que el recortador sepa qué tipo conservar en la salida. Es probable que este código haya funcionado antes del recorte (siempre y cuando la entrada sea algo que se sabe que existe en el marco de destino), pero probablemente produciría una excepción de referencia nula después del recorte, ya que Type.GetType devuelve null cuando no se encuentra el tipo.

En este caso, el recortador emite una advertencia en la llamada a Type.GetType, que indica que no puede determinar qué tipo va a usar la replicación.

Reacción a advertencias de recorte

Las advertencias de recorte están pensadas para aportar predictibilidad al recorte. Hay dos grandes categorías de advertencias que probablemente verá:

  1. La funcionalidad no es compatible con el recorte
  2. La funcionalidad tiene ciertos requisitos en la entrada para ser compatible con el recorte

Funcionalidad no compatible con el recorte

Suelen ser métodos que, o bien no funcionan en absoluto, o bien pueden fallar en determinados casos si se usan en una aplicación recortada. Un buen ejemplo es el método Type.GetType del ejemplo anterior. Podría funcionar en una aplicación recortada, pero no hay ninguna garantía. Estas API se marcan con RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute es simple y amplio: es un atributo que indica que el miembro se ha anotado como incompatible con el recorte. Este atributo se utiliza cuando el código no es fundamentalmente compatible con el recorte, o la dependencia del recorte es demasiado compleja para explicarla al recortador. Esto suele ser cierto para los métodos que cargan código dinámicamente, por ejemplo, a través de LoadFrom(String); enumeran o buscan en todos los tipos de una aplicación o ensamblado, por ejemplo, mediante GetType(); usan la palabra clave de C# dynamic; o usan otras tecnologías de generación de código en tiempo de ejecución. Un ejemplo sería:

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

No hay muchas soluciones alternativas para RequiresUnreferencedCode. La mejor solución consiste en evitar llamar al método al recortar y usar otro sistema que sea compatible con el recorte.

Marcado de una funcionalidad como no compatible con el recorte

Si está escribiendo una biblioteca y no está bajo su control si se va a usar o no una funcionalidad incompatible, puede marcarla con RequiresUnreferencedCode. Esto anota el método como incompatible con el recorte. Usar RequiresUnreferencedCode silenciará todas las advertencias de recorte en el método dado, pero producirá una advertencia cada vez que otra persona lo llame.

RequiresUnreferencedCodeAttribute requiere que especifique un Message. El mensaje se muestra como parte de una advertencia notificada al desarrollador que llama al método marcado. Por ejemplo:

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

Con el ejemplo anterior, una advertencia de un método específico podría tener este aspecto:

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

Por lo general, los desarrolladores que llaman a estas API no van a estar interesados en las particularidades de la API afectada ni en los detalles específicos relacionados con el recorte.

Un buen mensaje debe indicar qué funcionalidad no es compatible con el recorte y, a continuación, guiar al desarrollador sobre cuáles son los posibles pasos siguientes. Puede sugerir usar otra funcionalidad o cambiar la manera en que se usa la funcionalidad. También puede limitarse a indicar que la funcionalidad aún no es compatible con el recorte sin ofrecer un reemplazo claro.

Si las instrucciones para el desarrollador resultan demasiado largas para incluirlas en un mensaje de advertencia, puede agregar una Url opcional a RequiresUnreferencedCodeAttribute para que remita al desarrollador a una página web donde se describa el problema y las posibles soluciones con mayor detalle.

Por ejemplo:

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

Esto genera una advertencia:

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

El uso de RequiresUnreferencedCode suele provocar que se marquen más métodos con ella, por la misma razón. Esto es habitual cuando un método de alto nivel es incompatible con el recorte porque llama a un método de bajo nivel que no es compatible con el recorte. La advertencia se "propaga" a una API pública. Cada uso de RequiresUnreferencedCode necesita un mensaje y, en estos casos, es probable que los mensajes sean los mismos. Para evitar duplicar cadenas y para que el mantenimiento sea más fácil, use un campo de cadena constante para almacenar el mensaje:

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

Funcionalidad con requisitos en su entrada

El recorte proporciona API para especificar más requisitos sobre la entrada a métodos y otros miembros que conducen a código compatible con recortes. Estos requisitos suelen ser estar relacionados con la reflexión y la capacidad de acceder a determinados miembros u operaciones en un tipo. Estos requisitos se especifican mediante DynamicallyAccessedMembersAttribute.

A diferencia de RequiresUnreferencedCode, el recortador a veces puede entender la reflexión siempre que se anote correctamente. Echemos un vistazo al ejemplo original:

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

En el ejemplo anterior, el problema real es Console.ReadLine(). Debido a que se podría leer cualquier tipo, el recortador no tiene forma de saber si necesita métodos en System.DateTime o System.Guid o cualquier otro tipo. Por otro lado, el código siguiente sería correcto:

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

Aquí el recortador puede ver el tipo exacto al que se hace referencia: System.DateTime. Ahora puede usar el análisis de flujo para determinar que necesita mantener todos los métodos públicos en System.DateTime. Entonces, ¿dónde entra DynamicallyAccessMembers? Cuando la reflexión se divide entre varios métodos. En el código siguiente podemos ver que el tipo System.DateTime fluye a Method3, donde se usa la reflexión para acceder a los 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();
    ...
}

Si compila el código anterior, se genera la siguiente advertencia:

IL2070: Program.Method3(Type): el argumento "this" no satisface "DynamicallyAccessedMemberTypes.PublicMethods" en la llamada a "System.Type.GetMethods()". El parámetro "type" del método "Program.Method3(Type)" no tiene anotaciones coincidentes. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Por razones de rendimiento y estabilidad, el análisis de flujo no se realiza entre métodos, por lo que se necesita una anotación para pasar información de uno a otro, desde la llamada a la reflexión (GetMethods) hasta el origen de Type. En el ejemplo anterior, la advertencia del recortador está diciendo que GetMethods requiere que la instancia de objeto Type a la que se llama tenga la anotación PublicMethods, pero la variable type no tiene el mismo requisito. En otras palabras, es necesario pasar los requisitos de GetMethods hasta el autor de la llamada:

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

Después de anotar el parámetro type, desaparece la advertencia original, pero aparece otra:

IL2087: el argumento "type" no satisface "DynamicallyAccessedMemberTypes.PublicMethods" en la llamada a "Program.Method3(Type)". El parámetro genérico "T" de "Program.Method2<T>()" no tiene anotaciones coincidentes.

Propagamos las anotaciones hasta el parámetro type de Method3; en Method2 tenemos un problema parecido. El recortador puede realizar un seguimiento del valor T a medida que fluye por la llamada a typeof, se asigna a la variable local t y se pasa a Method3. En ese momento, ve que el parámetro type requiere PublicMethods, pero no hay ningún requisito en T y genera una nueva advertencia. Para corregir esto, debemos "anotar y propagar" aplicando anotaciones por la cadena de llamadas hasta que lleguemos a un tipo conocido estáticamente (como System.DateTime o System.Tuple) u otro valor anotado. En este caso, es necesario anotar el 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();
  ...
}

Ahora no hay advertencias porque el recortador sabe a qué miembros se puede acceder mediante la reflexión en tiempo de ejecución (métodos públicos) y en qué tipos (System.DateTime), y los conservará. Se recomienda agregar anotaciones para que el recortador sepa qué conservar.

Las advertencias generadas por estos requisitos adicionales se suprimen automáticamente si el código afectado está en un método con RequiresUnreferencedCode.

A diferencia de RequiresUnreferencedCode, que simplemente informa de la incompatibilidad, agregar DynamicallyAccessedMembers hace que el código sea compatible con el recorte.

Supresión de las advertencias del recortador

Si de algún modo puede determinar que la llamada es segura y no se recortará todo el código necesario, también puede suprimir la advertencia mediante UnconditionalSuppressMessageAttribute. Por ejemplo:

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

Advertencia

Tenga mucho cuidado al suprimir las advertencias de recorte. Es posible que la llamada ahora sea compatible con el recorte, pero esto puede cambiar según vaya modificando el código y es posible que olvide revisar todas las supresiones.

UnconditionalSuppressMessage es como SuppressMessage, pero lo pueden ver publish y otras herramientas posteriores a la compilación.

Importante

No use SuppressMessage ni #pragma warning disable para suprimir las advertencias del recortador. Solo funcionan para el compilador, pero no se conservan en el ensamblado compilado. El recortador funciona en ensamblados compilados y no vería la supresión.

La supresión se aplica al cuerpo completo del método. Por lo tanto, en nuestro ejemplo anterior suprime todas las advertencias IL2026 del método. Esto dificulta la comprensión, ya que no está claro qué método es el problemático a menos que agregue un comentario. Lo más importante es que, si el código cambia en el futuro, por ejemplo, si ReportResults también se vuelve incompatible con el recorte, no se notifica ninguna advertencia para esta llamada de método.

Para resolverlo, refactorice la llamada de método problemática en un método independiente o una función local y, luego, aplique la supresión solo a ese 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
    }
}