Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

13 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 11 de febrero de 2020
.NET Core Como sabemos, para ejecutar cualquier tipo de aplicación .NET Core en un equipo necesitamos tener instalado el runtime o el SDK de la plataforma. Esto es algo que podemos hacer muy fácilmente, simplemente acudiendo a la página oficial de descargas, eligiendo nuestro sistema operativo y siguiendo las instrucciones de instalación.

El hecho de que en el equipo destino esté preinstalado el runtime es muy interesante, entre otras cosas porque permite asegurar de antemano que en él se encontrarán todas las dependencias (frameworks, bibliotecas, paquetes, metapaquetes) necesarios para una correcta ejecución. Por tanto, para distribuir nuestra aplicación sólo debemos generar lo relativo a nuestro código, el resto ya estará allí.
Esta forma de publicar aplicaciones se denomina framework-dependent, pues dependen de que determinados componentes del framework estén instalado en el destino.
Por ejemplo, el paquete de publicación de una aplicación de consola prácticamente vacía, que únicamente muestra el mensaje "Hello world!", ocuparía solo 175K:
D:\MyConsoleApp\output>dir

 El volumen de la unidad D es Datos
 El número de serie del volumen es: 8CBC-81E3

 Directorio de D:\MyConsoleApp\output

09/02/2020  18:47    <DIR>          .
09/02/2020  18:47    <DIR>          ..
09/02/2020  18:46               428 MyConsoleApp.deps.json
09/02/2020  18:46             4.608 MyConsoleApp.dll
09/02/2020  18:46           169.984 MyConsoleApp.exe
09/02/2020  18:46               668 MyConsoleApp.pdb
09/02/2020  18:46               154 MyConsoleApp.runtimeconfig.json
               5 archivos        175.842 bytes
               2 dirs  463.058.874.368 bytes libres

D:\MyConsoleApp\output>_
Otra ventaja de este tipo de distribución es que es cross platform pura, es decir, podemos copiar los archivos a cualquiera de los sistemas operativos soportados y, siempre que dispongan del runtime, nuestra aplicación podrá correr sobre ellos sin problema.

Y todo esto está muy bien, pero, ¿qué pasa si quiero crear una aplicación portable, de forma que pueda distribuirla y ejecutarla sin necesidad de que el equipo destino tenga nada preinstalado?

Pues eso es lo que veremos en este post ;)

Publicación self-contained

Este tipo de publicación incluye todo lo necesario para que la aplicación funcione, sin necesidad de que el equipo destino disponga de componentes preinstalados. Es la opción más segura si desconocemos qué runtimes están instalados en el servidor, o incluso si queremos aislarnos de cambios o actualizaciones que pudieran instalarse en el servidor y que, de alguna forma, pudieran salpicarnos en el futuro.

Para publicar en modo self contained desde Visual Studio, sólo tendremos que acudir al perfil de publicación y editar sus settings de la siguiente forma:

Perfil de publicación estableciendo el deployment mode a self-contained y el target runtime a win-x64

Como se observa en la captura anterior, en el Deployment mode debemos seleccionar "Self-contained", mientras que en la opción Target Runtime tendremos que indicar el runtime sobre el que vamos a ejecutar la aplicación. Este paso es importante, pues hará que los binarios a incluir en el paquete distribuible sean los específicos para el entorno indicado.

Esto también podemos hacerlo si queremos generar el paquete de publicación desde la línea de comandos, por ejemplo como sigue:
D:\MyConsoleApp>dotnet publish --self-contained -r win-x64 -c release -o output

Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.

  Restauración realizada en 138,79 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
  MyConsoleApp -> D:\MyConsoleApp\bin\release\netcoreapp3.1\win-x64\MyConsoleApp.dll
  MyConsoleApp -> D:\MyConsoleApp\output\

D:\MyConsoleApp\output>_
En cualquier caso, el principal inconveniente de este modelo de distribución es que los archivos a mover son bastantes más, por lo que los despliegues serán más lentos y ocuparán mucho más espacio en disco. Por ejemplo, si hacemos un dir en la carpeta de resultados de la publicación veremos que hemos pasado de tener cinco archivos a más de doscientos, y el tamaño de menos de 200Kb a 69Mb:
D:\MyConsoleApp\output>dir

 El volumen de la unidad D es Datos
 El número de serie del volumen es: 8CBC-81E3

 Directorio de D:\MyConsoleApp\output

09/02/2020  18:56    <DIR>          .
09/02/2020  18:56    <DIR>          ..
20/04/2018  06:28            19.208 api-ms-win-core-console-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-datetime-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-debug-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-errorhandling-l1-1-0.dll
20/04/2018  06:29            22.280 api-ms-win-core-file-l1-1-0.dll
20/04/2018  06:37            18.696 api-ms-win-core-file-l1-2-0.dll
       (... Omitidos más de 200 archivos ...)
09/12/2019  03:39            14.928 System.Xml.XmlDocument.dll
09/12/2019  03:40            16.768 System.Xml.XmlSerializer.dll
09/12/2019  03:40            14.200 System.Xml.XPath.dll
09/12/2019  03:39            15.952 System.Xml.XPath.XDocument.dll
20/04/2018  06:37         1.016.584 ucrtbase.dll
09/12/2019  03:40            15.440 WindowsBase.dll
             225 archivos     69.122.606 bytes
               2 dirs  462.918.475.776 bytes libres

D:\MyConsoleApp\output>_
Sí, pensaréis que es un exceso para mostrar un simple "Hola mundo", pero si tenemos en cuenta que estos archivos incluyen el framework y todas sus bibliotecas no sale tan mal parada la cosa. Lo importante en este caso es que podemos copiar esta carpeta a cualquier servidor Windows x64 y funcionará correctamente, sin necesidad de tener nada preinstalado.

Pero aún podemos mejorarlo un poco...

Eliminando peso: Trimming de paquetes

Cuando hemos echado un ojo al contenido de la carpeta de destino de la publicación, vimos archivos como System.Xml.XmlSerializer.dll o System.Xml.XPath.dll... ¿realmente necesitamos desplegar estos ensamblados en nuestra aplicación, que sólo muestra un "Hello world!"? Seguro que no.

Desde .NET Core 3.0, el SDK incluye de serie una funcionalidad que permite eliminar de los archivos distribuibles los paquetes que no sean utilizados por nuestra aplicación o sus dependencias. Para activarlo, basta con añadir el elemento <PublishTrimmed> en el archivo .csproj del proyecto:
<PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
Hecho esto, si volvemos a repetir la operación de publicación, veremos que el paquete de publicación se reducirá bastante:
D:\MyConsoleApp>dotnet publish --self-contained -r win-x64 -c release -o output

Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.

  Restauración realizada en 26,72 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
  MyConsoleApp -> D:\MyConsoleApp\bin\Release\netcoreapp3.1\win-x64\MyConsoleApp.dll
  Se está optimizando el tamaño de los ensamblados, lo que puede cambiar el 
  comportamiento de la aplicación. Asegúrese de probarlo después de publicar. 
  Consulte https://aka.ms/dotnet-illink
  MyConsoleApp -> D:\MyConsoleApp\output\

D:\MyConsoleApp>dir output

 El volumen de la unidad D es Datos
 El número de serie del volumen es: 8CBC-81E3

 Directorio de D:\MyConsoleApp\output
09/02/2020  19:05    <DIR>          .
09/02/2020  19:05    <DIR>          ..
20/04/2018  06:28            19.208 api-ms-win-core-console-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-datetime-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-debug-l1-1-0.dll
20/04/2018  06:28            18.696 api-ms-win-core-errorhandling-l1-1-0.dll
       (... Omitidos más de 50 archivos ...)
09/02/2020  19:05            62.464 System.Console.dll
07/12/2019  16:40         9.555.840 System.Private.CoreLib.dll
09/02/2020  19:05            74.752 System.Runtime.Extensions.dll
20/04/2018  06:37         1.016.584 ucrtbase.dll
              63 archivos     26.499.046 bytes
               2 dirs  462.951.739.392 bytes libres

D:\MyConsoleApp>_
Mucho mejor ahora: hemos pasado de 225 archivos a 63, y reducido el peso de 69 a 26Mb. Aunque sigue siendo demasiado para un simple "Hello world!", al menos sabemos que es el mínimo al que podemos aspirar si queremos que el paquete distribuible de nuestra aplicación incluya el framework sobre el que será ejecutada.

Un último detalle: como hemos comentado anteriormente, el trimming eliminará los paquetes que detecte que no son utilizados, pero puede haber ocasiones en las que nos interesa que este mecanismo no elimine algún ensamblado en particular. Por ejemplo, si nuestra aplicación utiliza reflexión o cualquier otro mecanismo para cargar o utilizar ensamblados, el trimmer no los detectará y asumirá que no se están usando, lo que provocará errores en tiempo de ejecución.

Para indicar que un ensamblado debe ser incluido obligatoriamente en el paquete de publicación, podemos usar el elemento <TrimmerRootAssembly> en el .csproj:
<ItemGroup>
  <TrimmerRootAssembly Include="System.Xml.XmlSerializer.dll" />
</ItemGroup>

Publicación single-file: ¡un único ejecutable!

A partir de .NET Core 3, tenemos disponible un nuevo modelo de distribución que permite incluir en un único ejecutable todo lo necesario para que nuestra aplicación funcione sin tener nada preinstalado.

Podemos publicar como archivo único mediante una orden de la CLI como la siguiente:
D:\MyConsoleApp>dotnet publish -r win10-x64 -p:PublishSingleFile=true -o output

Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.

  Restauración realizada en 124,52 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
  MyConsoleApp -> D:\MyConsoleApp\bin\Debug\netcoreapp3.1\win10-x64\MyConsoleApp.dll
  Se está optimizando el tamaño de los ensamblados, lo que puede cambiar el 
  comportamiento de la aplicación. Asegúrese de probarlo después de publicar. 
  Consulte https://aka.ms/dotnet-illink
  MyConsoleApp -> D:\MyConsoleApp\output\

D:\MyConsoleApp>dir output

 El volumen de la unidad D es Datos
 El número de serie del volumen es: 8CBC-81E3

 Directorio de D:\MyConsoleApp\output

09/02/2020  19:10    <DIR>          .
09/02/2020  19:10    <DIR>          ..
09/02/2020  19:10        26.501.267 MyConsoleApp.exe
09/02/2020  19:10               680 MyConsoleApp.pdb
               2 archivos     26.501.947 bytes
               2 dirs  462.872.199.168 bytes libres

D:\MyConsoleApp>_
En lugar de tener que indicar tantos parámetros cada vez, también podríamos conseguirlo modificando el archivo .csproj. Para ello, bastaría con establecer a true el elemento <PublishSingleFile>, aunque al hacerlo, además, será obligatorio introducir el runtime de destino en el item <RuntimeIdentifier>. El resultado final podría ser algo así (trimming incluido):
<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <PublishSingleFile>true</PublishSingleFile>
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
En cualquier caso, fijaos que podríamos distribuir la aplicación, el runtime y el framework en un único ejecutable de 26Mb (usando el trimmer).

Publicado en Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

6 Comentarios:

MontyCLT dijo...

Hola José María.

Creo que es interesante indicar que en el último caso, el de distribuir la aplicación completa en un único ejecutable, durante la primera ejecución, esta extrae todos los ensamblados y sus dependencias en un directorio temporal, lo que da como resultado que la primera ejecución tarda unos segundos extra en arrancar.

José María Aguilar dijo...

Hola!

Muchas gracias por la aportación :) En efecto, así es; afortunadamente, como indicas, esto solo ocurre la primera vez que la ejecutas, por lo que esos segundos extra son asumibles en muchos casos.

Un saludo!

Benjamin Camacho dijo...

¿Se puede usar esto en contenedores Docker? Recién hice un hola mundo en Docker con C# y Go. La imagen de Go pesa 6.5 MB y la de C# 105MB y ya usando Alpine.

José María Aguilar dijo...

Hola, Benjamín!

Pues sí, debería funcionar con docker, aunque he de decirte que el "single-file" no lo he probado. Si tienes ocasión de hacerlo, espero que puedas escribirlo en tu blog o al menos comentar por aquí si has encontrado algún inconveniente :)

Saludos!

Unknown dijo...

Excelente artículo, como siempre. Comentar solamente que el "single-file" no es compatible con la publicación en IIS

José María Aguilar dijo...

Muchas gracias por la aportación, desconocía ese detalle :)

Saludos!

Artículos relacionados: