Saltar al contenido

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

17 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 Core, MVC, Blazor, 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.

13 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!

Rubén dijo...

No me funciona con la plantilla básica de Visual Studio :-(
Habilito stdoutLogEnabled="true" en el web.config y ninguna pista. Solo recibo un error 500, alguna pista? Gracias de antemano!

Rubén dijo...

Me respondo a mi mismo:

Tras publicar, en el fichero web.config que se genera automáticamente he tenido que cambiar el valor de modules="AspNetCoreModuleV2" a modules="AspNetCoreModule". Me resulta extraño que tenga que hacer esto cada vez que publico... no sé si sería posible establecerlo de alguna manera con anterioridad.

Por cierto,
Una vez más gran artículo!

Un saludo

José María Aguilar dijo...

Hola, Rubén!

¿Probaste a establecer el nombre del módulo en el .csproj mediante el tag AspNetCoreModule?

Saludos!

Rubén dijo...

Sí pero... aparece este error "TransformWebConfig task failed unexpectedly. In process hosting is not supported for AspNetCoreModule. Change the AspNetCoreModule to at least AspNetCoreModuleV2" :-(

Por otra parte,
Al publicar con la opción "single-file" un API en la que utilizo Swagger utilizando el fichero XML para incluir comentarios, no incluye este fichero físicamente y por tanto no funciona. Quizás lo embeba de alguna manera en el ejecutable...

Gracias por tu ayuda!

José María Aguilar dijo...

Hola!

Pues tiene sentido, porque la v2 fue la que introdujo el modo "in-process". En cualquier caso, puede que este tipo de hosting en IIS sea incompatible con el single-file y tenga que ser utilizado como out of process.

Respecto al XML, no lo he probado, pero debería haber alguna forma... ¿probaste a copiarlo, aunque sea a mano, en el directorio del exe? Si funcionase de esta forma, más adelante podrías hacer esa copia a mano a través de tags del csproj.

Juan Bastidas dijo...

Complementando lo que comenta Benjamín y José María para el caso de docker una buena manera es con los siguientes comandos en el Dockerfile, usando alpine para tener un contenedor mucho más liviano


FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine3.12
...
RUN dotnet restore -r linux-musl-x64
...
RUN dotnet publish -c Release -o /app -r linux-musl-x64

# Y tener cuidado en el entrypoint que debería quedar así:
ENTRYPOINT ["./"]

Esto entendiendo que sea linux, usando alpine vas a tener un contenedor 60% más liviano que el estándar de .net que es basado en Debian.

Un saludo, gracias José María por el artículo.

Juanjo M dijo...

Un artículo ameno y súper bien explicado. Muchísimas gracias.