.NET: tratamiento de dependencia

¿Quién no ha encontrado problemas debido a la redirección de ensamblado? Lo más probable es que todos los que desarrollaron una aplicación relativamente grande tarde o temprano se enfrenten a este problema.

Ahora trabajo en JetBrains, en el proyecto JetBrains Rider, y estoy involucrado en la tarea de migrar Rider a .NET Core. Anteriormente participó en infraestructura compartida en Circuit, una plataforma de alojamiento de aplicaciones basada en la nube.



Debajo de la escena se encuentra la transcripción de mi informe de la conferencia DotNext 2019 de Moscú, donde hablé sobre las dificultades al trabajar con asambleas en .NET y mostré con ejemplos prácticos lo que sucede y cómo lidiar con él.


En todos los proyectos donde trabajé como desarrollador de .NET, tuve que lidiar con varios problemas con la conexión de dependencias y la carga de ensamblajes. Hablaremos de esto.

Estructura del poste:


  1. Problemas de dependencia
  2. Estricto equipo de carga

  3. .NET Core

  4. Depurar descargas de ensamblados


¿Cuáles son algunos problemas de dependencia?


Cuando comenzaron a desarrollar .NET Framework a principios de la década de 2000, el problema del infierno de dependencias ya era conocido, cuando en todas las bibliotecas los desarrolladores permiten cambios importantes, y estas bibliotecas se vuelven incompatibles para su uso con código ya compilado. ¿Cómo resolver tal problema? La primera solución es obvia. Mantenga siempre la compatibilidad con versiones anteriores. Por supuesto, esto no es muy realista, porque romper el cambio es muy fácil de incluir en el código. Por ejemplo:



cambios de última hora y bibliotecas .NET

Este es un ejemplo específico de .NET. Tenemos un método y decidimos agregar un parámetro con un valor predeterminado. El código continuará compilándose si lo reensamblamos, pero binario serán dos métodos completamente diferentes: un método tiene cero argumentos, el segundo método tiene un argumento. Si el desarrollador dentro de la dependencia rompió la compatibilidad con versiones anteriores de esta manera, entonces no podremos usar el código que se compiló con esta dependencia en la versión anterior.

La segunda solución a los problemas de dependencia es agregar versiones de bibliotecas, ensamblajes, cualquier cosa. Puede haber diferentes reglas de versiones, el punto es que de alguna manera podemos distinguir diferentes versiones de la misma biblioteca entre sí, y usted puede entender si la actualización se romperá o no. Desafortunadamente, tan pronto como presentamos las versiones, aparece un tipo diferente de problema.



La versión hell es la incapacidad de usar una dependencia que sea compatible con binarios, pero al mismo tiempo tiene una versión que no se ajusta al tiempo de ejecución u otro componente que verifica estas versiones. En .NET, una manifestación típica de la versión hell es FileLoadException, aunque el archivo se encuentra en el disco, pero por alguna razón no está cargado con tiempo de ejecución.



En .NET, los ensamblados tienen muchas versiones diferentes: intentaron corregir los infiernos de versiones de varias maneras y ver qué sucedió. Tenemos un paquete System.Collections.Immutable. Mucha gente lo conoce. Tiene la última versión del paquete NuGet 1.6.0. Contiene una biblioteca, un ensamblado con la versión 1.2.4.0. Ha recibido que no tiene una biblioteca de compilación versión 1.2.4.0. ¿Cómo entender que se encuentra en el paquete NuGet 1.6.0? No será fácil. Además de la versión de ensamblaje, esta biblioteca tiene varias versiones más. Por ejemplo, Versión de archivo de ensamblaje, Versión de información de ensamblaje. Este paquete NuGet en realidad contiene tres ensamblajes diferentes con las mismas versiones (para diferentes versiones del estándar .NET).

Documentación .NET
Opbuild estándar

Se ha escrito mucha documentación sobre cómo trabajar con ensamblados en .NET. Existe una Guía .NET para desarrollar aplicaciones modernas para .NET teniendo en cuenta .NET Framework, .NET Standard, .NET Core, código abierto y todo lo que puede ser. Alrededor del 30% de todo el documento está dedicado a cargar ensamblajes. Analizaremos problemas específicos y ejemplos que puedan surgir.

¿Por qué es todo esto necesario? En primer lugar, para evitar pisar un rastrillo. En segundo lugar, puede facilitar la vida de los usuarios de sus bibliotecas porque con su biblioteca no tendrán los problemas de dependencia a los que están acostumbrados. También lo ayudará a hacer frente a la migración de aplicaciones complejas a .NET Core. Y para colmo, puede convertirse en un SRE, este es un ingeniero de redireccionamiento senior (vinculante), al que todos en el equipo vienen y preguntan cómo escribir otra redirección.

Montaje estricto Cargando


La carga estricta del ensamblado es el principal problema que enfrentan los desarrolladores en .NET Framework. Se expresa en FileLoadException. Antes de continuar con la carga del ensamblaje estricto, permíteme recordarte algunas cosas básicas.

Cuando crea una aplicación .NET, la salida es un artefacto, que generalmente se encuentra en Bin / Debug o en Bin / Release, y contiene un cierto conjunto de ensamblajes de ensamblaje y archivos de configuración. Los ensamblajes se referirán entre sí por nombre, nombre de ensamblaje. Es importante comprender que los enlaces de ensamblaje se encuentran directamente en el ensamblaje que hace referencia a este ensamblaje; no hay archivos de configuración mágicos donde se escriben las referencias de ensamblaje. Aunque le parezca que tales archivos existen. Las referencias se encuentran en los propios ensamblados en forma binaria.

En .NET, hay un proceso de resolución de ensamblaje: esto es cuando la definición del ensamblado ya se ha convertido en un ensamblaje real, que está en el disco o cargado en algún lugar de la memoria. La resolución de ensamblaje se realiza dos veces: en la etapa de compilación, cuando tiene referencias en * .csproj, y en tiempo de ejecución, cuando tiene referencias dentro de los ensamblajes, y según algunas reglas, se convierten en ensamblajes que se pueden descargar.

// Nombre simple
MyAssembly, Version = 6.0.0.0,
Culture = neutral, PublicKeyToken = null

// Nombre
seguro Newtonsoft.Json, Version = 6.0.0.0,
Culture = neutral, PublicKeyToken = 30ad4fe6b2a6aeed // PublicKey


Pasemos al problema. Nombre de la asamblea hay dos tipos principales. El primer tipo de nombre de ensamblado es Nombre simple. Son fáciles de identificar por el hecho de que tienen PublicKeyToken = null. Hay un nombre Strong, es fácil identificarlos por el hecho de que su PublicKeyToken no es nulo, sino que tiene algún valor.



Pongamos un ejemplo. Tenemos un programa que depende de la biblioteca con las utilidades MyUtils, y la versión de MyUtils es 9.0.0.0. El mismo programa tiene un enlace a otra biblioteca. Esta biblioteca también quiere usar MyUtils, pero la versión 6.0.0.0. MyUtils versión 9.0.0.0 y versión 6.0.0.0 tienen PublicKeyToken = null, es decir, tienen un nombre simple. ¿Qué versión caerá en el artefacto binario, 6.0.0.0 o 9.0.0.0? Novena versión. ¿Puede MyLibrary usar MyUtils versión 9.0.0.0, que entró en el artefacto binario?



De hecho, puede, porque MyUtils tiene un nombre simple y, en consecuencia, la carga de ensamblaje estricta no existe para él.



Otro ejemplo. En lugar de MyUtils, tenemos una biblioteca completa de NuGet, que tiene un nombre Strong. La mayoría de las bibliotecas en NuGet tienen un nombre fuerte.



En la etapa de compilación, la versión 9.0.0.0 se copia a BIN, pero en tiempo de ejecución obtenemos la famosa FileLoadException. Para que MyLibrary, que quiere la versión 6.0.0.0 Newtonsoft.Json, pueda usar la versión 9.0.0.0, debe ir y escribir Redireccionamiento de enlace App.config.

Redireccionamientos vinculantes





Redirección de versiones de ensamblaje

Establece que un ensamblaje con dicho nombre y publicKeyToken debe redirigirse de un rango de versiones a un rango de versiones. Parece ser un registro muy simple, pero sin embargo se encuentra aquí App.config, pero podría estar en otros archivos. Hay un archivo machine.configdentro de .NET Framework, dentro del tiempo de ejecución, en el que se define un conjunto estándar de redireccionamientos, que pueden diferir de una versión a otra de .NET Framework. Puede suceder que en 4.7.1 nada funcione para usted, pero en 4.7.2 ya funciona, o viceversa. .App.configDebe tener en cuenta que los redireccionamientos pueden provenir no solo del suyo , y esto debe tenerse en cuenta al depurar.

Simplificamos la escritura de redireccionamientos


Nadie quiere escribir redireccionamientos vinculantes con sus manos. ¡Démosle esta tarea a MSBuild!



Cómo habilitar y deshabilitar la redirección automática de enlaces

Algunos consejos sobre cómo simplificar el trabajo con la redirección de enlaces. Consejo uno: habilite la generación automática de redireccionamiento de enlace en MSBuild. Activado por propiedad en *.csproj. Al construir un proyecto, caerá en un artefacto binario App.config, que indica redirecciones a versiones de bibliotecas que están en el mismo artefacto. Esto solo funciona para ejecutar aplicaciones, aplicaciones de consola, WinExe. Para las bibliotecas, esto no funciona, porque para las bibliotecasApp.configla mayoría de las veces simplemente no es relevante, porque es relevante para una aplicación que se inicia y carga ensamblajes. Si realizó una configuración para la biblioteca, entonces en la aplicación algunas dependencias también pueden diferir de las que existían al construir la biblioteca, y resulta que la configuración para la biblioteca no tiene mucho sentido. Sin embargo, a veces para las bibliotecas las configuraciones todavía tienen sentido.



La situación cuando escribimos pruebas. Las pruebas generalmente se encuentran en ClassLibrary y también necesitan redireccionamientos. Los marcos de prueba pueden reconocer que la biblioteca con pruebas tiene una configuración dll e intercambiar las redirecciones que se encuentran en ellas por el código de las pruebas. Puede generar estos redireccionamientos automáticamente. Si tenemos un formato antiguo*.csproj, no al estilo SDK, puede seguir el camino simple, cambiar el OutputType a Exe y agregar un punto de entrada vacío, esto obligará a MSBuild a generar redireccionamientos. Puedes ir hacia otro lado y usar el truco. Puede agregar otra propiedad a *.csproj, lo que hace que MSBuild considere que para este OutputType todavía necesita generar redireccionamientos de enlace. Este método, aunque parece un truco, le permitirá generar redireccionamientos para bibliotecas que no se pueden rehacer en Exe, y para otros tipos de proyectos (excepto pruebas).

Para el nuevo formato, los *.csprojredireccionamientos se generarán ellos mismos si usa Microsoft.NET.Test.Sdk moderno.

Tercer consejo: no use la generación de redireccionamiento de enlace con NuGet. NuGet tiene la capacidad de generar redireccionamiento de enlace para bibliotecas que pasan de paquetes a las últimas versiones, pero esta no es la mejor opción. Todos estos redireccionamientos deberán agregarse App.configy confirmarse, y si genera redireccionamientos utilizando MSBuild, los redireccionamientos se generarán durante la compilación. Si los cometes, es posible que tengas conflictos de fusión. Usted mismo puede simplemente olvidar actualizar la redirección de enlace en el archivo, y si se generan durante la compilación, no lo olvidará.



Resolver referencia de ensamblaje
Generar redireccionamientos de enlace

Tarea para aquellos que desean comprender mejor cómo funciona la generación de redireccionamientos de enlace: descubra cómo funciona, vea esto en el código. Vaya al directorio .NET, vaya a todas partes con la propiedad de nombre, que se utiliza para habilitar la generación. Este es generalmente un enfoque tan común, si hay alguna propiedad extraña para MSBuild, puede ir y aprovechar su uso. Afortunadamente, la propiedad generalmente se usa en configuraciones XML, y puede encontrar fácilmente su uso.

Si examina lo que hay en estos destinos XML, verá que esta propiedad activa dos tareas de MSBuild. Se llama a la primera tarea ResolveAssemblyReferencesy genera un conjunto de redireccionamientos que se escriben en los archivos. La segunda tarea GenerateBindingRedirectsescribe los resultados de la primera tarea enApp.config. Existe una lógica XML que corrige ligeramente el funcionamiento de la primera tarea y elimina algunos redireccionamientos innecesarios, o agrega otros nuevos.

Alternativa a las configuraciones XML


No siempre es conveniente mantener las redirecciones en la configuración XML. Es posible que tengamos una situación en la que la aplicación descarga el complemento, y este complemento utiliza otras bibliotecas que requieren redireccionamientos. En este caso, es posible que no conozcamos el conjunto de redireccionamientos que necesitamos o que no queramos generar XML. En tal situación, podemos crear un AppDomain y, cuando se crea, aún transferirlo a donde se encuentra el XML con las redirecciones necesarias. También podemos manejar errores de carga de ensamblajes en tiempo de ejecución. Rantime .NET brinda esa oportunidad.

AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => 
{ 
   var name = eventArgs.Name; 
   var requestingAssembly = eventArgs.RequestingAssembly; 
   
   return Assembly.LoadFrom(...); // PublicKeyToken should be equal
};


Tiene un evento, se llama CurrentDomain.AssemblyResolve. Al suscribirse a este evento, recibiremos errores sobre todas las descargas fallidas de ensamblajes. Obtenemos el nombre del ensamblaje que no se cargó, y obtenemos el ensamblaje de ensamblaje que solicitó que se cargara el primer ensamblaje. Aquí podemos cargar manualmente el ensamblaje desde el lugar correcto, por ejemplo, descartando la versión, simplemente tomándola del archivo y devolviendo este evento desde el controlador. O devuelva nulo si no tenemos nada que devolver, si no podemos cargar el ensamblado. PublicKeyToken debería ser el mismo, los ensamblados con diferentes PublicKeyToken de ninguna manera son amigos entre sí.



Este evento se aplica solo a un dominio de aplicación. Si nuestro complemento crea un AppDomain dentro de sí mismo, esta redirección en tiempo de ejecución no funcionará en ellos. Debe suscribirse de alguna manera a este evento en todos los AppDomain que creó el complemento. Podemos hacer esto usando el AppDomainManager.

AppDomainManager es un ensamblado separado que contiene una clase que implementa una interfaz específica, y uno de los métodos de esta interfaz le permitirá inicializar cualquier nuevo AppDomain que se cree en la aplicación. Una vez que se crea el AppDomain, se llamará a este método. En él puedes suscribirte a este evento.

Estricto montaje de carga y .NET Core


En .NET Core no hay ningún problema llamado "Carga estricta de ensamblados", que se debe al hecho de que los ensamblados firmados requieren exactamente la versión solicitada. Hay otro requisito. Para todos los ensamblados, independientemente de si están firmados con un nombre seguro o no, se verifica que la versión que se cargó en tiempo de ejecución sea mayor o igual que la anterior. Si estamos en una situación de una aplicación con complementos, podemos tener una situación tal que el complemento se creó, por ejemplo, a partir de una nueva versión del SDK, y la aplicación en la que se descarga utiliza la versión anterior del SDK hasta ahora, y en lugar de desmoronarse, También podemos suscribirnos a este evento, pero ya en .NET Core, y también cargar el ensamblado que tenemos. Podemos escribir este código:
AppDomain.CurrentDomain.AssemblyResolve += (s, eventArgs) => 
{ 
     CheckForRecursion(); 
     var name = eventArgs.Name;
     var requestingAssembly = eventArgs.RequestingAssembly; 
    
     name.Version = new Version(0, 0); 
     
     return Assembly.Load(name); 
};


Tenemos el nombre del ensamblado que no se inició, anulamos la versión y la llamamos Assembly.Loaddesde la misma versión. No habrá recursividad aquí, porque ya verifiqué la recursividad.



Era necesario descargar MyUtils versión 0.0.2.0. En BIN, tenemos MyUtils versión 0.0.1.0. Realizamos una redirección de la versión 0.0.2.0 a la versión 0.0. La versión 0.0.1.0 no se cargará con nosotros. Nos saldrá una salida indicando que no fue posible cargar el ensamblaje con la versión 0.0.2 16–1 . 2 16-1 .

new Version(0, 0) == new Version(0, 0, -1, -1) 

class Version { 
     readonly int _Build; 
     readonly int _Revision; 
     readonly int _Major; 
     readonly int _Minor; 
} 
(ushort) -1 == 65535


En la clase Versión, no todos los componentes son obligatorios, y en lugar de los componentes opcionales –1 se almacenan, pero en algún lugar del interior, se produce un desbordamiento y se obtienen los 2 16–1 . Si está interesado, puede intentar encontrar exactamente dónde se produce el desbordamiento.



Si trabaja con ensamblajes de reflexión y desea obtener todos los tipos, puede resultar que no todos los tipos puedan obtener su método GetTypes. Un ensamblado tiene una clase que hereda de otra clase que está en un ensamblaje que no está cargado.

static IEnumerable GetTypesSafe(this Assembly assembly) 
{ 
    try 
    { 
        return assembly.GetTypes(); 
    }
    catch (ReflectionTypeLoadException e) 
   { 
        return e.Types.Where(x => x != null); 
    } 
}



En este caso, el problema será que se lanzará una ReflectionTypeLoadException. En el interior ReflectionTypeLoadExceptionhay una propiedad en la que hay esos tipos que aún lograron cargarse. No todas las bibliotecas populares tienen esto en cuenta. AutoMapper, al menos una de sus versiones, si se enfrenta a ReflectionTypeLoadException, simplemente se cayó, en lugar de ir y elegir los tipos desde el interior de la excepción.

Nombramiento fuerte


Ensamblados con nombre seguro

Hablemos sobre las causas de la carga de ensamblados estrictos, este es el nombre Fuerte.
Nombre seguro es la firma del ensamblado por parte de una clave privada que utiliza cifrado asimétrico. PublicKeyToken es el hash de clave pública de este ensamblado.

Strong Naming le permite distinguir entre diferentes ensamblados que tienen el mismo nombre. Por ejemplo, MyUtils no es un nombre único, puede haber varios ensamblados con ese nombre, pero si firma el nombre Strong, tendrán diferentes PublicKeyToken y podemos distinguirlos de esta manera. Se requiere un nombre seguro para algunos escenarios de carga de ensamblaje.

Por ejemplo, para instalar un ensamblaje en la Caché de ensamblados global o para descargar varias versiones de lado a lado a la vez. Lo que es más importante, los conjuntos con nombre seguro solo pueden hacer referencia a otros conjuntos con nombre seguro. Dado que algunos usuarios desean firmar sus compilaciones con el nombre Strong, los desarrolladores de la biblioteca también firman sus bibliotecas, para que sea más fácil para los usuarios instalarlas, de modo que los usuarios no tengan que volver a firmar estas bibliotecas.

Nombre fuerte: ¿Legado?


Nomenclatura fuerte y bibliotecas .NET

Microsoft dice explícitamente en MSDN que no debe usar un nombre fuerte con fines de seguridad, que solo proporcionan para distinguir diferentes ensamblados con el mismo nombre. La clave de ensamblaje no se puede cambiar de ninguna manera; si la cambió, interrumpirá las redirecciones a todos sus usuarios. Si tiene una parte privada de la clave para el nombre Strong filtrada al acceso público, entonces no puede retirar esta firma de ninguna manera. El formato de archivo SNK en el que se encuentra el nombre seguro no ofrece esa oportunidad, y otros formatos para almacenar claves al menos contienen un enlace a la Lista de revocación de certificados CRL, por lo que se puede entender que este certificado ya no es válido. No hay nada de eso en SNK.

La guía de código abierto tiene las siguientes recomendaciones. En primer lugar, adicionalmente por razones de seguridad, use otras tecnologías. En segundo lugar, si tiene una biblioteca de código abierto, generalmente se sugiere que confirme la parte privada de la clave en el repositorio, para que sea más fácil para las personas bifurcar su biblioteca, reconstruirla y ponerla en una aplicación lista para usar. En tercer lugar, nunca cambie el nombre de Strong. Demasiado destructivo. A pesar de que es demasiado destructivo y está escrito al respecto en la guía de código abierto, Microsoft a veces tiene problemas con sus propias bibliotecas.



Hay una biblioteca llamada System.Reactive. Anteriormente, estos eran varios paquetes NuGet, uno de ellos es Rx-Linq. Esto es solo un ejemplo, lo mismo para el resto de los paquetes. En la segunda versión, se firmó con una clave de Microsoft. En la tercera versión, se mudó al repositorio en el proyecto github.com/dotnet y comenzó a tener una firma de .NET Foundation. La biblioteca, de hecho, ha cambiado el nombre de Strong. Se cambió el nombre del paquete NuGet, pero el ensamblado se llama dentro exactamente igual que antes. ¿Cómo redirigir de la segunda versión a la tercera? Esta redirección no se puede hacer.

Validación de nombre fuerte


Cómo: Deshabilitar la función de omisión de nombre seguro

Otro argumento de que el nombre Strong ya es algo del pasado y sigue siendo puramente formal es que no están validados. Tenemos un ensamblado firmado y queremos corregir algún tipo de error, pero no tenemos acceso a las fuentes. Podemos tomar dnSpy: esta es una utilidad que le permite descompilar y reparar ensamblajes ya compilados. Todo funcionará para nosotros. Porque de manera predeterminada, la omisión de validación de nombre seguro está habilitada, es decir, solo verifica que PublicKeyToken sea igual y no se verifica la integridad de la firma. Puede haber estudios ambientales en los que la firma aún se verifique, y aquí un ejemplo vívido es IIS. La integridad de la firma se verifica en IIS (el bypass de validación de nombre seguro está deshabilitado de forma predeterminada), y todo se romperá si editamos el ensamblado firmado.

Adición:Puede deshabilitar la verificación de firma para el ensamblado mediante el signo público. Con él, solo se utiliza la clave pública para la firma, lo que garantiza la seguridad del nombre del ensamblado. Las claves públicas utilizadas por Microsoft se publican aquí .
En Rider, el signo público se puede habilitar en las propiedades del proyecto.





Cuándo cambiar las versiones de ensamblaje de archivos

La guía de código abierto también ofrece algunas políticas de control de versiones, cuyo objetivo es reducir la cantidad de redireccionamientos y cambios de enlace necesarios para los usuarios en NET Framework. Esta política de versiones es que no debemos cambiar la versión de ensamblaje constantemente. Esto, por supuesto, puede generar problemas con la instalación en el GAC, por lo que la imagen nativa instalada puede no corresponder al ensamblado y tendrá que realizar la compilación JIT nuevamente, pero, en mi opinión, esto es menos malo que los problemas con el control de versiones. En el caso de CrossGen, los ensamblajes nativos no se instalan globalmente, no habrá problemas.

Por ejemplo, el paquete NuGet Newtonsoft.Json tiene varias versiones: 12.0.1, 12.0.2, etc. Todos estos paquetes tienen un ensamblaje con la versión 12.0.0.0. La recomendación es que la versión de ensamblaje se actualice cuando cambie una versión principal del paquete NuGet.

recomendaciones


Siga los consejos para .NET Framework: genere redireccionamientos manualmente e intente usar la misma versión de dependencias en todos los proyectos de su solución. Esto debería minimizar significativamente la cantidad de redireccionamientos. Necesita nombres seguros solo si tiene un escenario de carga de compilación específico donde es necesario, o si está desarrollando una biblioteca y desea simplificar la vida de los usuarios que realmente necesitan nombres fuertes. No cambie el nombre fuerte.

Estándar .NET


Pasamos a .NET Standard. Está bastante relacionado con la versión hell en .NET Framework. .NET Standard es una herramienta para escribir bibliotecas que son compatibles con diversas implementaciones de la plataforma .NET. Las implementaciones se refieren a .NET Framework, .NET Core, Mono, Unity y Xamarin.



* Enlace a la documentación

Esta es la tabla de soporte de .NET Standard para varias versiones de diferentes versiones de tiempos de ejecución. Y aquí podemos ver que .NET Framework de ninguna manera es compatible con .NET Standard versión 2.1. El lanzamiento de .NET Framework, que admitirá .NET Standard 2.1 y versiones posteriores, aún no está planeado. Si está desarrollando una biblioteca y desea que funcione para los usuarios en .NET Framework, deberá tener un objetivo para .NET Standard 2.0. Además del hecho de que .NET Framework no admite la última versión de .NET Standard, prestemos atención al asterisco. .NET Framework 4.6.1 es compatible con .NET Standard 2.0, pero con un asterisco. Hay una nota al pie de página directamente en la documentación, ¿de dónde saqué esta tabla?



Considere un proyecto de ejemplo. Una aplicación en .NET Framework que tiene una dependencia dirigida al estándar .NET. Algo así: ConsoleApp y ClassLibrary. Target Library .NET Standard. Cuando organicemos este proyecto, será así en nuestro BIN.



Tendremos cien DLL allí, de los cuales solo uno está relacionado con la aplicación, todo lo demás vino para admitir el estándar .NET. El hecho es que .NET Standard 2.0 apareció más tarde que .NET Framework 4.6.1, pero al mismo tiempo resultó ser compatible con API, y los desarrolladores decidieron agregar compatibilidad con Standard 2.0 a .NET 4.6.1. No lo hicimos de forma nativa (por inclusión netstandard.dllen el tiempo de ejecución en sí), sino de tal manera que .NET Standard * .dll y todas las demás fachadas de ensamblaje se colocan directamente en BIN.



Si observamos las dependencias de la versión de .NET Framework a la que nos dirigimos y la cantidad de bibliotecas que cayeron en el BIN, veremos que no hay tantas en 4.7.1, y desde 4.7.2 no hay bibliotecas adicionales, y .NET Estándar es compatible allí de forma nativa.



Este es un tweet de uno de los desarrolladores de .NET, que describe este problema y recomienda usar .NET Framework versión 4.7.2 si tenemos bibliotecas .NET Standard. Ni siquiera con la versión 2.0 aquí, sino con la versión 1.5.

recomendaciones


Si es posible, eleve el Marco de destino en su proyecto al menos a 4.7.1, preferiblemente 4.7.2. Si está desarrollando una biblioteca para facilitar la vida de los usuarios de la biblioteca, cree un Target separado para .NET Framework, evitará una gran cantidad de dlls que pueden entrar en conflicto con algo.

.NET Core


Comencemos con una teoría general. Discutiremos cómo lanzamos JetBrains Rider en .NET Core, y por qué deberíamos hablar sobre eso. Rider es un proyecto muy grande, tiene una gran solución empresarial con una gran cantidad de proyectos diferentes, un complejo sistema de dependencias, no puede simplemente tomarlo y migrar a otro tiempo de ejecución al mismo tiempo. Para hacer esto, tenemos que usar algunos hacks, que también analizamos.

Aplicación .NET Core


¿Cómo es una aplicación típica de .NET Core? Depende de cómo se implemente exactamente, a qué se destinará en última instancia. Podemos tener varios escenarios. El primero es una implementación dependiente de Framework. Esto es lo mismo que en .NET Framework cuando la aplicación usa el tiempo de ejecución preinstalado en la computadora. Puede ser una implementación autónoma, esto es cuando la aplicación lleva un tiempo de ejecución. Y puede haber una implementación de un solo archivo, esto es cuando obtenemos un archivo exe, pero en el caso de .NET Core dentro de este archivo exe hay un artefacto de aplicación autónoma, este es un archivo autoextraíble.



Solo consideraremos la implementación dependiente de Framework. Tenemos un dll con la aplicación, hay dos archivos de configuración, el primero de los cuales es obligatorio, este runtimeconfig.jsonydeps.json. Comenzando con .NET Core 3.0, se genera un archivo exe que es necesario para que la aplicación sea más conveniente de ejecutar, de modo que no necesite ingresar el comando .NET si estamos en Windows. Las dependencias se incluyen en este artefacto, comenzando con .NET Core 3.0, en .NET Core 2.1 necesita publicar o usar otra propiedad *.csproj.

Marcos compartidos, .runtimeconfig.json





.runtimeconfig.jsoncontiene la configuración de tiempo de ejecución necesaria para ejecutarlo. Indica en qué marco compartido se iniciará la aplicación, y se ve así. Indicamos que la aplicación se ejecutará en "Microsoft.NETCore.App" versión 3.0.0, puede haber otro Marco compartido. Otras configuraciones también pueden estar aquí. Por ejemplo, puede habilitar el servidor Recolector de basura.



.runtimeconfig.jsongenerado durante el montaje del proyecto. Y si queremos incluir el servidor GC, entonces tenemos que modificar de alguna manera este archivo por adelantado, incluso antes de armar el proyecto, o agregarlo a mano. Puede agregar su configuración aquí de esta manera. Podemos incluir propiedades en *.csproj, si dicha propiedad es proporcionada por desarrolladores de .NET, o si no se proporciona la propiedad, podemos crear un archivo llamadoruntimeconfig.template.jsony escriba la configuración necesaria aquí. Durante el ensamblaje, se agregarán otras configuraciones necesarias a esta plantilla, por ejemplo, el mismo Marco compartido.



Shared Framework es un conjunto de tiempo de ejecución y bibliotecas. De hecho, lo mismo que el tiempo de ejecución de .NET Framework, que solía instalarse una vez en la máquina y para todos era una versión. Shared Framework y, a diferencia de un solo tiempo de ejecución de .NET Framework, se puede versionar, diferentes aplicaciones pueden usar diferentes versiones de tiempos de ejecución instalados. También se puede heredar Framework compartido. El Marco compartido en sí se puede ver en las ubicaciones del disco que generalmente están instaladas en el sistema.



Existen varios marcos compartidos estándar, por ejemplo, Microsoft.NETCore.App, que ejecuta aplicaciones de consola convencionales, AspNetCore.App, para aplicaciones web, y WindowsDesktop.App, el nuevo Marco compartido en .NET Core 3, que ejecuta aplicaciones de escritorio. en Windows Forms y WPF. Los dos últimos Shared Framework esencialmente complementan el primero necesario para las aplicaciones de consola, es decir, no tienen un tiempo de ejecución completamente nuevo, sino que simplemente complementan el existente con las bibliotecas necesarias. Parece que esta herencia también está en los directorios de Shared Framework runtimeconfig.jsonen los que se especifica el Shared Framework base.

Manifiesto de dependencia ( .deps.json)



Sondeo predeterminado - .NET Core El

segundo archivo de configuración es este .deps.json. Este archivo contiene una descripción de todas las dependencias de la aplicación o el Marco compartido, o la biblioteca, las bibliotecas .deps.jsontambién lo tienen. Contiene todas las dependencias, incluidas las transitivas. Y el comportamiento del tiempo de ejecución de .NET Core difiere dependiendo de si .deps.jsonla aplicación lo tiene o no. De lo .deps.jsoncontrario, la aplicación podrá cargar todos los ensamblados que se encuentran en su Marco compartido o en su directorio BIN. Si lo hay .deps.json, entonces la validación está habilitada. Si uno de los ensamblados que figuran en la lista .deps.jsonno lo está, la aplicación simplemente no se iniciará. Verá el error presentado anteriormente. Si la aplicación intenta cargar algún ensamblaje en tiempo de ejecución, que.deps.json si, por ejemplo, utilizando métodos de carga de ensamblaje o durante el proceso de resolución de ensamblajes, verá un error muy similar a la carga de ensamblaje estricto.

Jinete de Jetbrains


Rider es un .NET IDE. No todos saben que Rider es un IDE que consiste en una interfaz basada en IntelliJ IDEA y escrita en Java y Kotlin, y un backend. El backend es esencialmente R #, que puede comunicarse con IntelliJ IDEA. Este backend es una aplicación multiplataforma .NET ahora.
¿A dónde corre? Windows usa .NET Framework, que está instalado en la computadora del usuario. En otros sistemas de información, en Linux y Mac, se usa Mono.

Esta no es una solución ideal cuando hay diferentes tiempos de ejecución en todas partes, y quiero pasar al siguiente estado para que Rider se ejecute en .NET Core. Para mejorar el rendimiento, porque en .NET Core todas las características más recientes están asociadas con esto. Para reducir el consumo de memoria. Ahora hay un problema con el funcionamiento de Mono con la memoria.

Cambiar a .NET Core le permitirá abandonar las tecnologías heredadas y no compatibles y le permitirá corregir algunas soluciones a los problemas que se encontraron en tiempo de ejecución. Cambiar a .NET Core le permitirá controlar la versión del tiempo de ejecución, es decir, Rider ya no se ejecutará en .NET Framework que está instalado en la computadora del usuario, sino en una versión específica de .NET Core, que puede prohibirse, en forma de una implementación autónoma. La transición a .NET Core eventualmente permitirá el uso de nuevas API que se importan específicamente en Core.

Ahora, el objetivo es lanzar un prototipo, lanzarlo, solo para comprobar cómo funcionará, cuáles son los posibles puntos de falla, qué componentes tendrán que reescribirse nuevamente, lo que requerirá un procesamiento global.

Características que dificultan la traducción de Rider a .NET Core


Visual Studio, incluso si R # no está instalado, se bloquea por falta de memoria en soluciones grandes, dentro de las cuales hay proyectos con SDK-style * .csproj . SDK-style * .csproj es una de las principales condiciones para una reubicación completa de .NET Core.

Esto es un problema porque Rider se basa en R #, viven en el mismo repositorio, los desarrolladores de R # quieren usar Visual Studio para desarrollar su propio producto en su producto para que sea un alimento. En R # hay bibliotecas específicas de enlaces para el marco con el que debe hacer algo. En Windows, podemos usar el Framework para aplicaciones de escritorio, y en Linux y Mac, Mock ya se usa para bibliotecas de Windows con una funcionalidad mínima.

Decisión


Decidimos quedarnos con los viejos por ahora *.csproj, ensamblarlos bajo el Framework completo, pero como los ensamblajes de Framework y Core son compatibles con binarios, ejecútelos en Core. No utilizamos funciones incompatibles, agregamos todos los archivos de configuración necesarios manualmente y descargamos versiones especiales de dependencias para .NET Core, si las hay.

¿A qué hacks has tenido que ir?


Un truco: queremos llamar a un método que solo está disponible en Framework, por ejemplo, este método es necesario en R #, pero no en Core. El problema es que si no hay un método, el método que lo llama durante la compilación JIT caerá antes MissingMethodException. Es decir, un método que no existe ha arruinado el método que lo llama.

static void Method() { 
  if (NetFramework) 
     CallNETFrameworkOnlyMethod();

  ... 
} 
[MethodImpl(MethodImplOptions.NoInlining)] 
static void CallNETFrameworkOnlyMethod() { 
  NETFrameworkOnlyMethod(); 
}


La solución está aquí: hacemos llamadas a métodos incompatibles en métodos separados. Hay un problema más: dicho método puede volverse en línea, por lo tanto, lo marcamos con un atributo NoInlining.

Hack número dos: necesitamos poder cargar ensamblajes en rutas relativas. Tenemos un ensamblaje para Framework, hay una versión especial para .NET Core. ¿Cómo descargamos la versión .NET Core para .NET Core?



Nos ayudarán .deps.json. Veamos la .deps.jsonbiblioteca System.Diagnostics.PerformanceCounter. Tal biblioteca es notable en términos de su.deps.json. Tiene una sección de tiempo de ejecución, en la que se indica una versión de la biblioteca con su ruta relativa. Esta biblioteca, el ensamblado se cargará en todos los tiempos de ejecución, y solo arroja las ejecuciones. Si, por ejemplo, se carga en Linux, PerformanceCounter no funciona en el diseño en Linux, y una excepción PlatformNotSupportedException vuela desde allí. También hay .deps.jsonuna sección runtimeTargets en esto y aquí ya se indica la versión de este ensamblado específicamente para Windows, donde PerformanceCounter debería funcionar.

Si tomamos la sección de tiempo de ejecución y escribimos en ella la ruta relativa a la biblioteca que queremos cargar, esto no nos ayudará. La sección de tiempo de ejecución en realidad establece la ruta relativa dentro del paquete NuGet, y no en relación con el BIN. Si buscamos este ensamblado en BIN, solo se usará el nombre del archivo desde allí. La sección runtimeTargets ya contiene una ruta relativa honesta, una ruta honesta relativa a BIN. Prescribiremos una ruta relativa para nuestros ensamblados en la sección runtimeTargets. En lugar del identificador de tiempo de ejecución, que es "ganar" aquí, podemos tomar otro que nos guste. Por ejemplo, escribiremos el identificador de tiempo de ejecución "any", y este ensamblado se cargará generalmente en todas las plataformas. O escribiremos "unix", y arrancará en Linux, y en Mac, y así sucesivamente.

Siguiente truco: queremos descargar en Linux y en Mac Mock para construir WindowsBase. El problema es que el ensamblado llamado WindowsBase ya está presente en Shared Framework Microsoft.NETCore.App, incluso si no estamos en Windows. En el Marco compartido de Windows, Microsoft.WindowsDesktop.AppWindowsBase redefine la versión en la que se encuentra NETCore.App. Miremos .deps.jsonestos Framework, más precisamente en aquellas secciones que describen WindowsBase.



Aquí está la diferencia:



si alguna biblioteca entra en conflicto y está presente en varias .deps.json, entonces se selecciona el máximo de ellas para el par formado por assemblyVersiony fileVersion. La guía .NET dice que fileVersionsolo es necesario mostrarlo en el Explorador de Windows, pero no es así, cae en.deps.json. Este es el único caso que conozco cuando la versión prescrita .deps.json, assemblyVersiony fileVersion, realmente se utilizan. En todos los demás casos, vi un comportamiento que, independientemente de las versiones .deps.jsonescritas, el ensamblado continuaría cargándose de todos modos.



Cuarto truco. Tarea: tenemos un archivo .deps.json para los dos hacks anteriores, y lo necesitamos solo para dependencias específicas. Dado que se .deps.jsongeneran en modo semi-manual, tenemos un script que, de acuerdo con alguna descripción de lo que debería llegar allí, lo genera durante la compilación, queremos mantener esto lo más .deps.jsonmínimo posible para que podamos entender lo que contiene . Queremos deshabilitar la validación y permitir la descarga de ensamblajes que están en el BIN pero que no se describen en .deps.json.

Solución: habilite la configuración personalizada en runtimeconfig. Esta configuración es realmente necesaria para la compatibilidad con versiones anteriores de .NET Core 1.0.

recomendaciones


Entonces, .runtime.jsony .deps.jsonen .NET Core, estos son una especie de análogos App.config. App.configle permite hacer lo mismo, por ejemplo, cargar ensamblajes de manera relativa. Utilizando .deps.json, reescribiéndolo manualmente, puede personalizar la carga de ensamblados en .NET Core, si tiene un escenario muy complejo.

Depurar descargas de ensamblados


Hablé sobre algunos tipos de problemas, por lo que debe ser capaz de depurar problemas al cargar ensamblajes. ¿Qué puede ayudar con esto? Primero, los tiempos de ejecución escriben registros sobre cómo cargan los ensamblados. En segundo lugar, puedes mirar más de cerca las ejecuciones que vuelan hacia ti. También puede centrarse en eventos de tiempo de ejecución.

Registros de fusión





Volver a lo básico: uso del visor de registro de Fusion para depurar errores oscuros
Fusion

El mecanismo para cargar ensamblados en .NET Framework se llama Fusion y sabe cómo registrar lo que hizo en el disco. Para habilitar el registro, debe agregar configuraciones especiales al registro. Esto no es muy conveniente, por lo que tiene sentido usar utilidades, a saber, Fusion Log Viewer y Fusion ++. Fusion Log Viewer es una utilidad estándar que viene con Visual Studio y se puede iniciar desde la línea de comandos de Visual Studio, Visual Studio Developer Command Prompt. Fusion ++ es un análogo de código abierto de esta herramienta con una interfaz más agradable.



Fusion Log Viewer se ve así. Esto es peor que WinDbg porque esta ventana ni siquiera se estira. Sin embargo, puede perforar las marcas de verificación aquí, aunque no siempre es obvio qué conjunto de marcas de verificación es correcto.



Fusion ++ tiene un botón "Iniciar registro", y luego aparece el botón "Detener registro". En él, puede ver todos los registros sobre la carga de ensamblajes, leer los registros sobre lo que estaba sucediendo exactamente. Estos registros se parecen a esto de una manera concisa.



Esta es una ejecución de la carga estricta del ensamblado. Si miramos los registros de Fusion, veremos que necesitábamos descargar la versión 9.0.0.0 después de procesar todas las configuraciones. Encontramos un archivo en el que se sospecha que tenemos el ensamblaje que necesitamos. Vimos que la versión 6.0.0.0 está en este archivo. Tenemos una advertencia de que comparamos los nombres completos de los ensamblados, y difieren en la versión principal. Y luego se produjo un error: la versión no coincide.

Eventos de tiempo de ejecución





Registro de eventos de tiempo de ejecución

En Mono, puede habilitar el registro utilizando variables de entorno, y los registros eventualmente se escribirán en stdouty stderr. No es tan conveniente, pero la solución está funcionando.



Sondeo predeterminado:
documentación de .NET Core / documentos de diseño / rastreo de host

. .NET Core también tiene una variable de entorno especial COREHOST_TRACEque incluye el inicio de sesión stderr. Con .NET Core 3.0, puede escribir registros en un archivo especificando la ruta en una variable COREHOST_TRACEFILE.


Hay un evento que se dispara cuando los ensamblajes no se cargan. Se trata de un evento AssembleResolve. Hay un segundo evento útil, este FirstChanceException. Puede suscribirse y obtener un error sobre la carga de ensamblajes, incluso si alguien escribió try..catch y se perdió todas las ejecuciones en el lugar dondeFileLoadExceptionocurrió. Si la aplicación ya se ha compilado, puede iniciarla perfview, y puede monitorear las ejecuciones de .NET, y encontrar las relacionadas con la descarga de archivos allí.

recomendaciones


Transfiera el trabajo a herramientas, a herramientas de desarrollo, a un IDE, a MSBuild, que le permite generar redireccionamientos. Puede cambiar a .NET Core, luego olvidará lo que es la carga estricta de ensamblajes y podrá usar la nueva API tal como queremos lograrla en Rider. Si conecta la biblioteca .NET Standard, aumente la versión de destino de .NET Framework al menos a 4.7.1. Si pareces estar en una situación desesperada, busca hacks, úsalos o inventa tus propios hacks para situaciones desesperadas. Y armarse con herramientas de depuración.

Le recomiendo que lea los siguientes enlaces:



DotNext 2020 Piter . , 8 JUG Ru Group.

All Articles