Share via


Preparación de las bibliotecas de .NET para el recorte

El SDK de .NET permite reducir el tamaño de las aplicaciones independientes mediante el recorte. El recorte quita el código sin usar de la aplicación y sus dependencias. No todo el código es compatible con el recorte. En .NET 6 se proporcionan advertencias de análisis de recorte para detectar patrones que pueden interrumpir las aplicaciones recortadas. Este artículo:

Requisitos previos

SDK de .NET 6 o posterior.

Para obtener las advertencias de recorte más actualizadas y la cobertura del analizador:

  • Instale y use el SDK de .NET 8 o posterior.
  • Marque como destino net8.0 o posterior.

SDK de .NET 7 o posterior.

Para obtener las advertencias de recorte más actualizadas y la cobertura del analizador:

  • Instale y use el SDK de .NET 8 o posterior.
  • Marque como destino net8.0 o posterior.

SDK de .NET 8 o posterior.

Habilitación de advertencias de recorte de biblioteca

Se pueden encontrar advertencias de recorte en una biblioteca con cualquiera de los métodos siguientes:

  • Habilite el recorte específico de proyecto mediante la propiedad IsTrimmable.
  • Creación de una aplicación de prueba de recorte que use la biblioteca y habilitación del recorte para la aplicación de prueba. No es necesario hacer referencia a todas las API de la biblioteca.

Se recomienda usar ambos enfoques. El recorte específico del proyecto es cómodo y muestra advertencias de recorte para un proyecto, pero se basa en las referencias que se marcan como compatibles con el recorte para ver todas las advertencias. El recorte de una aplicación de prueba es más eficaz, pero muestra todas las advertencias.

Habilitación del recorte específico de proyecto

Establezca <IsTrimmable>true</IsTrimmable> en el archivo del proyecto.

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

Establecer la propiedad de MSBuild IsTrimmable en true marca el ensamblado como "recortable" y habilita las advertencias de recorte. "Recortable" significa que el proyecto:

  • Se considera compatible con el recorte.
  • No debe generar advertencias relacionadas con el recorte al compilar. Cuando se usa en una aplicación recortada, el ensamblado tiene sus miembros sin usar recortados en la salida final.

La propiedad IsTrimmable tiene como valor predeterminado true al configurar un proyecto como compatible con AOT con <IsAotCompatible>true</IsAotCompatible>. Para obtener más información, consulte Analizadores de compatibilidad AOT.

Para generar advertencias de recorte sin marcar el proyecto como compatible con el recorte, use <EnableTrimAnalyzer>true</EnableTrimAnalyzer> en lugar de <IsTrimmable>true</IsTrimmable>.

Mostrar todas las advertencias con la aplicación de prueba

Para mostrar todas las advertencias de análisis de una biblioteca, el recortador debe analizar la implementación de la biblioteca y de todas las dependencias que use la biblioteca.

Al compilar y publicar una biblioteca:

  • Las implementaciones de las dependencias no están disponibles.
  • Los ensamblados de referencia disponibles no tienen suficiente información para que el recortador determine si son compatibles con el recorte.

Debido a las limitaciones de dependencia, se debe crear una aplicación de prueba independiente que use la biblioteca y sus dependencias. La aplicación de prueba incluye toda la información que el recortador requiere para emitir una advertencia sobre las incompatibilidades de recorte en:

  • El código de la biblioteca.
  • Código al que hace referencia la biblioteca desde sus dependencias.

Nota:

Si la biblioteca tiene un comportamiento diferente en función del marco de la plataforma de destino, cree una aplicación de prueba de recorte para cada una de las plataformas de destino que admitan el recorte. Por ejemplo, si la biblioteca usa compilación condicional como #if NET7_0 para cambiar el comportamiento.

Para crear la aplicación de prueba de recorte:

  • Cree un proyecto de aplicación de consola separado.
  • Agregue una referencia a la biblioteca.
  • Modifique el proyecto similar al que se muestra a continuación mediante la lista siguiente:

Si la biblioteca tiene como destino un TFM que no se puede recortar, por ejemplo, net472 o netstandard2.0, no hay ninguna ventaja para crear una aplicación de prueba de recorte. El recorte solo se admite para .NET 6 y versiones posteriores.

  • Establezca <TrimmerDefaultAction> en link.
  • Agregue <PublishTrimmed>true</PublishTrimmed>.
  • Agregue una referencia al proyecto de biblioteca con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique la biblioteca como un ensamblado raíz del recortador con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantiza que se analizan todas las partes de la biblioteca. Indica al recortador que este ensamblado es una "raíz". Un ensamblado "raíz" significa que el optimizador analiza todas las llamadas de la biblioteca y recorre todas las rutas de acceso al código que se originan en ese ensamblado.
  • Agregue <PublishTrimmed>true</PublishTrimmed>.
  • Agregue una referencia al proyecto de biblioteca con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique la biblioteca como un ensamblado raíz del recortador con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantiza que se analizan todas las partes de la biblioteca. Indica al recortador que este ensamblado es una "raíz". Un ensamblado "raíz" significa que el optimizador analiza todas las llamadas de la biblioteca y recorre todas las rutas de acceso al código que se originan en ese ensamblado.
  • Agregue <PublishTrimmed>true</PublishTrimmed>.
  • Agregue una referencia al proyecto de biblioteca con <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Especifique la biblioteca como un ensamblado raíz del recortador con <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly garantiza que se analizan todas las partes de la biblioteca. Indica al recortador que este ensamblado es una "raíz". Un ensamblado "raíz" significa que el optimizador analiza todas las llamadas de la biblioteca y recorre todas las rutas de acceso al código que se originan en ese ensamblado.

Archivo .csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.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: En el archivo del proyecto anterior, al usar .NET 7, reemplace <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>

Una vez actualizado el archivo del proyecto, ejecute dotnet publish con el identificador de runtime (RID) al que desea dirigirse.

dotnet publish -c Release -r <RID>

También puede seguir el mismo patrón para varias bibliotecas. Para ver las advertencias de análisis de recorte de más de una biblioteca a la vez, agréguelas todas al mismo proyecto como elementos ProjectReference y TrimmerRootAssembly. Al añadir todas las bibliotecas al mismo proyecto con los elementos ProjectReference y TrimmerRootAssembly, se advierte sobre las dependencias si alguna de las bibliotecas raíz usan una API no compatible con recorte en una dependencia. Para ver las advertencias relacionadas con una biblioteca determinada, haga referencia solamente a esa biblioteca.

Nota: Los resultados del análisis dependen de los detalles de implementación de las dependencias. La actualización a una nueva versión de una dependencia puede presentar advertencias de análisis:

  • Si la nueva versión agregó patrones de reflexión no comprendidos.
  • Aunque no haya cambios en la API.
  • La introducción de advertencias de análisis de recorte es un cambio importante cuando se usa la biblioteca con PublishTrimmed.

Resolución de advertencias de recorte

Los pasos anteriores generan advertencias sobre el código que puede causar problemas cuando se usan en una aplicación recortada. En los ejemplos siguientes se muestran las advertencias más comunes con recomendaciones para corregirlas.

RequiresUnreferencedCode

Considere el siguiente código que usa [RequiresUnreferencedCode] para indicar que el método especificado necesita acceso dinámico al código al que no se hace referencia estáticamente, por ejemplo, a travé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()
    {
    }
}

El código destacado antes indica que la biblioteca llama a un método que se ha anotado de forma explícita como incompatible con el recorte. Para deshacerse de la advertencia, considere si MyMethod debe llamar a DynamicBehavior. En ese caso, anote también el autor de la llamada MyMethod con [RequiresUnreferencedCode]; esto hará que la advertencia se "propague", de modo que los autores de la llamada a MyMethod recibirán una advertencia en su lugar:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

Una vez que haya propagado el atributo hasta la API pública, las aplicaciones que llaman a la biblioteca:

  • Obtienen advertencias solo para los métodos públicos que no se pueden recortar.
  • No reciben advertencias 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())
        {
            // ...
        }
    }
}

En el código anterior, UseMethods llama a un método de reflexión que tiene un requisito [DynamicallyAccessedMembers]. El requisito indica que los métodos públicos del tipo están disponibles. Puede cumplir el requisito si agrega el mismo requisito al parámetro de UseMethods.

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

Ahora, las llamadas a UseMethods generan advertencias si pasan valores que no satisfacen el requisito de PublicMethods . Al igual que con [RequiresUnreferencedCode], una vez que haya propagado estas advertencias a las API públicas, ya ha terminado.

En el ejemplo siguiente, un Tipo desconocido fluye al parámetro del método anotado. El Type desconocido procede de un 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);
}

Del mismo modo, aquí el problema es que el campo type se ha pasado a un parámetro con estos requisitos. Puede corregirlo agregando [DynamicallyAccessedMembers] al campo. [DynamicallyAccessedMembers] advierte sobre el código que asigna valores incompatibles al campo. A veces, este proceso continuará hasta que se anota una API pública y otras veces finalizará cuando un tipo concreto fluya a una ubicación con estos requisitos. Por ejemplo:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

En este caso, el análisis de recorte mantiene los métodos públicos de Tupley genera más advertencias.

Recomendaciones

  • Evitar reflexión siempre que sea posible. Al usar la reflexión, limite el ámbito de reflexión para que solo sea accesible desde una pequeña parte de la biblioteca.
  • Anote el código con DynamicallyAccessedMembers para expresar estáticamente los requisitos de recorte siempre que sea posible.
  • Considere la posibilidad de reorganizar el código para que siga un patrón analizable que se pueda anotar con DynamicallyAccessedMembers
  • Cuando el código no es compatible con el recorte, anótelo con RequiresUnreferencedCode y propague esta anotación a los autores de llamadas hasta que se anotan las API públicas pertinentes.
  • Evite usar código que use la reflexión de una manera no comprendida por el análisis estático. Por ejemplo, se debe evitar la reflexión en constructores estáticos. El uso de una reflexión que no se puede analizar estáticamente en constructores estáticos da como resultado la propagación de advertencia a todos los miembros de la clase.
  • Evite anotar métodos virtuales o métodos de interfaz. Evite anotar métodos virtuales o de interfaz, para lo que será necesario que todas las invalidaciones tengan anotaciones correspondientes.
  • Si una API es principalmente incompatible con el recorte, es posible que sea necesario tener en cuenta enfoques de codificación alternativos a la API. Un ejemplo común son los serializadores basados en reflexión. En estos casos, considere la posibilidad de adoptar otra tecnología, como los generadores de código fuente, para generar código que se analice de forma estática con más facilidad. Por ejemplo, consulte Generación de origen en System.Text.Json

Resolución de advertencias para patrones no analizables

Es preferible resolver las advertencias mediante la expresión de la intención del código con [RequiresUnreferencedCode] y DynamicallyAccessedMembers siempre que sea posible. Sin embargo, en algunos casos, es posible que le interese habilitar el recorte de una biblioteca que usa patrones que no se pueden expresar con esos atributos o sin refactorizar código existente. En esta sección se describen algunas formas avanzadas de resolver las advertencias de análisis de recorte.

Advertencia

Estas técnicas pueden cambiar el comportamiento o el código o generar excepciones en tiempo de ejecución si se usan incorrectamente.

UnconditionalSuppressMessage

Considere el código con estas características:

  • La intención no se puede expresar con las anotaciones.
  • Genera una advertencia, pero no representa un problema real en tiempo de ejecución.

Las advertencias no se pueden suprimir UnconditionalSuppressMessageAttribute. Esto es similar a SuppressMessageAttribute, pero se conserva en lenguaje intermedio y se respeta durante el análisis de recorte.

Advertencia

Al suprimir las advertencias, es responsable de garantizar la compatibilidad de recorte del código en función de las invariables que sabe que son ciertas mediante la inspección y las pruebas. Tenga cuidado con estas anotaciones, ya que si son incorrectas o si cambian las invariables del código, podrían acabar ocultando problemas reales.

Por ejemplo:

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
{
}

En el código anterior, se ha anotado la propiedad del indizador para que el objeto Type devuelto cumpla los requisitos de CreateInstance. Esto ya garantiza que el constructor de TypeWithConstructor se mantenga y que la llamada a CreateInstance no genere una advertencia. La anotación del establecedor del indizador garantiza que los tipos almacenados en Type[] tengan un constructor. Pero el análisis no puede ver esto y sigue generando una advertencia para el captador, porque desconoce que el tipo devuelvo ha conservado su constructor.

Si está seguro de que se cumplen los requisitos, puede silenciar esta advertencia agregando [UnconditionalSuppressMessage] al captador:

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
{
}

Es importante subrayar que solo es válido suprimir una advertencia si hay anotaciones o código que garantizan que los miembros reflejados sean destinos visibles de reflexión. No es suficiente que el miembro fuese simplemente el destino de una llamada, campo o acceso a propiedades. Puede parecer que a veces se da el caso, pero este código es susceptible de fallar con el tiempo a medida que se agregan más optimizaciones de recorte. Las propiedades, los campos y los métodos que no son destinos visibles de reflexión se pueden insertar, quitar sus nombres, moverse a diferentes tipos o optimizar de otro modo de manera que se interrumpan los reflejos en ellos. Al suprimir una advertencia, solo se permite la reflexión en destinos que eran objetivos visibles de reflexión para el analizador de recorte en otro 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

El atributo de[DynamicDependency] se puede usar para indicar que un miembro tiene una dependencia dinámica de otros miembros. Esto hace que los miembros a los que se hace referencia se mantienen cada vez que se mantiene el miembro con el atributo, pero no silencia las advertencias por sí mismo. A diferencia de los demás atributos, que informan al análisis de recorte sobre el comportamiento de reflexión del código, [DynamicDependency] solo mantiene otros miembros. Esto se puede usar junto con [UnconditionalSuppressMessage] para corregir algunas advertencias de análisis.

Advertencia

Use el atributo de [DynamicDependency] solo como último recurso cuando los otros enfoques no sean viables. Es preferible expresar el comportamiento de reflexión del código mediante [RequiresUnreferencedCode] o [DynamicallyAccessedMembers].

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

Sin DynamicDependency, el recorte puede quitar Helper de MyAssembly o quitar MyAssembly por completo si no se hace referencia al mismo en otro lugar, lo que genera una advertencia que indica un posible error en tiempo de ejecución. El atributo garantiza que Helper se mantiene.

El atributo especifica los miembros que se mantienen mediante un objeto string o mediante DynamicallyAccessedMemberTypes. El tipo y el ensamblado están implícitos en el contexto del atributo o se especifican explícitamente en el atributo (mediante Type, o mediante objetos string para el tipo y el nombre del ensamblado).

Las cadenas de tipo y miembro usan una variación del formato de cadena de identificador de comentario de documentación de C#, sin el prefijo de miembro. La cadena de miembro no debe incluir el nombre del tipo declarante y puede omitir parámetros para mantener todos los miembros del nombre especificado. En el código siguiente, se muestran algunos ejemplos del formato:

[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<>))]

Este atributo [DynamicDependency] está diseñado para usarse en casos en los que un método contiene patrones de reflexión que no se pueden analizar incluso con la ayuda de DynamicallyAccessedMembersAttribute.