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, 29 de enero de 2019
ASP.NET CoreSin duda, entre las mejoras incluidas en ASP.NET Core 2.2 destaca especialmente el nuevo modelo de hosting in-process para IIS, que promete cuadriplicar el rendimiento prácticamente sin hacer ningún cambio en nuestras aplicaciones, siempre que las ejecutemos en un entorno Windows/IIS.

Como recordaréis, se trata de una mejora en el módulo ASP.NET Core (ANCM) que hace que las aplicaciones se ejecuten directamente dentro del worker del servidor web (w3wp.exe), evitando las llamadas internas producidas cuando IIS actuaba como un mero proxy inverso.

Por verlo gráficamente, el siguiente diagrama muestra la arquitectura tradicional de un sistema ASP.NET Core funcionando sobre IIS. En el modelo out-of-process utilizado hasta ASP.NET Core 2.1, cada petición realizada desde el exterior era capturada por IIS, que a su vez lanzaba una petición HTTP local con los mismos contenidos hacia Kestrel, que era quien la procesaba ejecutando nuestro código. La respuesta generada desde Kestrel era enviada de vuelta a IIS como respuesta a la petición, quien la retornaba al agente de usuario:

ASP.NET Core out-of-process

En este modelo de funcionamiento, por tanto, cada petición HTTP entrante generaba otra petición HTTP interna que, aunque estuviera dentro de la misma máquina, imponía una penalización importante en el rendimiento de las aplicaciones.

El hosting in-process, aparecido con ASP.NET Core 2.2, cambia las reglas del juego eliminando esas peticiones HTTP internas y los retardos que implicaban, lo que ha posibilitado el espectacular incremento en el rendimiento que su uso ofrece. Ahora, el módulo para IIS ejecuta internamente la aplicación y se comunica con ella de forma directa:

Hosting in-process

Este es el modo por defecto de los nuevos proyectos ASP.NET Core creados usando las plantillas estándar, algo que podemos claramente ver si echamos un vistazo al archivo .csproj de un proyecto recién creado, ya sea desde Visual Studio o desde .NET Core CLI:
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>
Si tenéis un proyecto ASP.NET Core 2.1 o anterior y lo actualizáis a 2.2, tendréis que añadir el elemento <AspNetCoreHostingModel> de forma manual para usar este modelo de ejecución.
Sin embargo, estas mejoras espectaculares no son gratis del todo, y creo que es interesante conocer un poco mejor lo que implica asumir este nuevo modo de funcionamiento de nuestras aplicaciones y responder a algunas preguntas que podríamos hacernos por el camino.

¿Qué implica usar el modo in-process?

Pues como casi todo en la vida, asumir este nuevo modo de funcionamiento tiene sus implicaciones y, a veces, contraindicaciones ;)

Por ejemplo, una consecuencia directa es que cuando usamos este modo desde Visual Studio, ya no podremos ver desde el propio entorno la salida de consola de la ejecución del proyecto; u otro ejemplo, ya no funcionará la compilación automática al cambiar archivos de código fuente. Si para nosotros estas son características importantes, lo mejor es continuar utilizando el hosting out-of-process, al menos durante el desarrollo.

También es importante un efecto colateral algo inesperado, y que puede dar lugar a problemas. Dado que ahora el proceso se ejecuta dentro del worker de IIS, el directorio por defecto retornado por Directory.GetCurrentDirectory() no será el de nuestra aplicación, sino el del propio IIS, como C:\Windows\System32\inetsrv o C:\Program Files\IIS Express. Si nuestro código depende en algún punto de que el directorio de ejecución sea la carpeta de los binarios, fallará.

Afortunadamente tiene una solución sencilla, pues si queremos dejarlo todo en su sitio sólo debemos actualizar el directorio actual con una llamada manual a Directory.SetCurrentDirectory() cuando la aplicación arranque. Probablemente en muchos escenarios nos valga con establecerlo al ContentRootPath ofrecido por IHostingEnvironment:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    Directory.SetCurrentDirectory(env.ContentRootPath);
    ...
}
Asimismo, como se indica en la documentación oficial del módulo ASP.NET Core, hay otra serie de cambios a tener en cuenta, entre otras:
  • El servidor que se utilizará internamente ya no es Kestrel, sino IISHttpServer. Salvo que estemos usando características específicas de Kestrel, esto no debería afectarnos mucho.
  • La arquitectura (x86 o x64) de la aplicación y el runtime deberá coincidir con el del pool de aplicaciones.
  • El atributo requestTimeout de la configuración del módulo ya no será válido, aunque tiene sentido, pues este atributo definía el timeout de la petición interna entre IIS y Kestrel.
  • No será posible compartir app pools entre distintas aplicaciones.
  • La parada de aplicaciones al hacer web deploy o usando app_offline.html puede retrasarse si hay conexiones abiertas.
En definitiva, el mensaje es que incluso mejoras espectaculares y apetecibles como el hosting in-process pueden venir cargadas de efectos colaterales que debemos tener en cuenta. Antes de adoptarlas hay ser prudentes y probarlas cuidadosamente en nuestros escenarios, para sólo dar el paso adelante cuando estemos seguros de que todo funciona con garantías.

Y para acabar con buen sabor de boca, añadiré un punto positivo: al usar el hosting in-process podremos detectar directamente las desconexiones de los clientes. Recordaréis que esto antes no era posible porque la desconexión física se producía en un servidor (IIS) distinto al que procesaba las peticiones (Kestrel), pero, al encontrarse ahora todo dentro del mismo proceso, podremos aprovechar esta característica para implementar lógica de cancelación de peticiones. De esta forma, podremos informarnos mediante un token de cancelación si el cliente desconectó, por ejemplo para evitar la continuación de un proceso cuya respuesta no va a ser recibida por nadie:
public Task<IActionResult> GenerateComplexReport(int id, CancellationToken cancellationToken)
{
    // cancellationToken will be cancelled when the client disconnects
    var report = await _reportGenerator.GenerateComplexReportAsync(id, cancellationToken);
    if (report == null)
    {
          return Empty();
    }
    return Ok(report);
}

¿Cómo puedo volver al modelo anterior, el hosting out-of-process?

Si por cualquier motivo quisiéramos desactivar el hosting in-process, bastaría como eliminar el elemento <AspNetCoreHostingModel> del archivo .csproj, o bien establecer su valor a OutOfProcess:
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
  </PropertyGroup>
De esta forma, todo seguiría funcionando tal y como lo hacía antes de la versión 2.2 de ASP.NET Core.

¿Podemos usar el modo in-process sólo en producción?

Esto podría ser interesante si preferimos utilizar el modelo out-of-process mientras desarrollamos, pero disfrutar del incremento brutal de rendimiento una vez pasemos a producción.

Aunque en un primer vistazo el modo de alojamiento parece depender directamente del valor que establezcamos en la propiedad <AspNetCoreHostingModel> del archivo del proyecto .csproj, en realidad este valor sólo se utiliza para generar apropiadamente el web.config que será utilizado por IIS para configurar el módulo ASP.NET Core al lanzar la aplicación. Por ejemplo, este es el web.config incluido al publicar un proyecto con el modo in-process configurado en su .csproj:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <location path="." inheritInChildApplications="false">
   <system.webServer>
     <handlers>
       <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
     </handlers>
     <aspNetCore processPath="dotnet" arguments=".\InProcessTest.dll" stdoutLogEnabled="false" 
                 stdoutLogFile=".\logs\stdout" hostingModel="InProcess" />
   </system.webServer>
 </location>
</configuration>
Por tanto, en tiempo de ejecución no tenemos forma de establecer uno u otro modelo de hosting porque cuando la aplicación arranque ya estará decidido, pero sí podemos hacerlo en tiempo de compilación o despliegue, que es cuando se genera el archivo web.config.

Para ello, podemos eliminar del archivo de proyecto .csproj el elemento <AspNetCoreHostingModel> y añadir el siguiente bloque, consiguiendo que el modelo de hosting sea in-process sólo cuando hayamos compilado con la configuración "Release" del proyecto, algo que normalmente ocurrirá cuando vayamos a desplegar a producción:
  <PropertyGroup Condition="'$(Configuration)' == 'Release' ">
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>
De esta forma, mientras estamos en modo "Debug" estaremos usando el hosting out-of-process, mientras que el modelo in-process sólo lo usaremos al ejecutar tras compilar o desplegar en modo "Release".

¿Cómo podemos saber en tiempo de ejecución si la aplicación se está ejecutando el modo in-process?

Pues si lo necesitáis para algo, la forma más sencilla, pero probablemente válida en muchos casos, sería consultar el nombre del proceso actualmente en ejecución con una llamada a Process.GetCurrentProcess().ProcessName. Cuando estamos utilizando el hosting in-process, el nombre del proceso será "w3wp" o "iisexpress", mientras que en out-of-process recibiremos "dotnet".

Pero también podemos hacerlo de forma algo más "pro" ;) Simplemente deberíamos determinar si el módulo ha sido cargado por el proceso actual utilizando la llamada GetModuleHandle() del kernel del sistema operativo (obviamente, sólo funcionará en Windows):
public static class AspNetCoreModuleHelpers
{
    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    public static bool IsInProcess()
    {
        return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 
               && GetModuleHandle("aspnetcorev2_inprocess.dll") != IntPtr.Zero;
    }
}
Así, el siguiente código retornará el modo de alojamiento cuando se haga una petición a "/mode":
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRouting();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseRouter(
            route => route.MapGet("mode",
                async ctx => await ctx.Response.WriteAsync(
                    "In-Process: " + AspNetCoreModuleHelpers.IsInProcess()
                )
            )
        );
    }
}
Espero que os haya resultado interesante, y que os ayude a sacar mayor provecho de esta interesante mejora de ASP.NET Core 2.2, o al menos hacerlo con mayor conocimiento de causa :)

Publicado en Variable not found.

12 Comentarios:

JorTurFer dijo...

Muy interesante José!
Una pregunta, en caso de habilitar la opción "In-progress", ¿como afectaría cuando lo despleguemos en un entorno no windows? Seguiría funcionando el kestrel normalmente o perdemos esa posibilidad?
Atte

José María Aguilar dijo...

Hola!

Pues aún no lo he probado, pero no debería haber problema ninguno porque esto sólo afecta a la generación del web.config que es usado únicamente por IIS. Por tanto, apostaría a que es totalmente transparente ;)

Saludos!

Javier Campos dijo...

A mí particularmente, lo de usar Directory.GetCurrentDirectory() siempre me ha dado grimilla (porque NUNCA tiene por qué ser la carpeta de los binarios), y hacerlo con Directory.SetCurrentDirectory(env.ContentRootPath); es un parche que me gusta también poco... si se usa ContentRootPath se usa eso, y si no, siempre abogo por usar algo como: new FileInfo(new Uri(Assembly.GetEntryAssembly().GetName().CodeBase).AbsolutePath).Directory, que devuelve un DirectoryInfo del entry point, esté donde esté, y no depende del estado (o de que alguien haya hecho un Directory.SetCurrentDirectory() por algún sitio y lo haya cambiado, o de que se arranque desde otra carpeta, o con un acceso directo, o con lo que sea).

Sólo un pequeño "tip" para eso... el artículo genial, como de costumbre, pero eso no hace falta que te lo diga ;-)

José María Aguilar dijo...

Hola, Javier!

Pues sí, sería más seguro hacerlo de esa forma para evitar efectos colaterales del cambio del directorio actual.

Muchas gracias por la aportación!

Arturo dijo...

Gracias José M. Ya habia cambiado mi aplicacion, de web api a In process de la 2.1 a la 2.2. Todo me funciona bien en local, sin problemas, pero al deplegar en una Api App de Azure no logré que funcionara. Será que aún no esta habilitado? Por cierto excelente artículo.

José María Aguilar dijo...

Hola!

Pues debería funcionarte, porque el despliegue a Azure, acabó a finales de diciembre, a no ser que despliegues sobre Linux (https://github.com/Azure/app-service-announcements/issues/151).

De hecho, acabo de probar con una aplicación Core MVC recién creada (con la plantilla por defecto), a la que he incluido el código que muestra qué tipo de hosting se está utilizando que hemos visto en este post, y al desplegar en Azure todo funciona correctamente y el hosting es in-process.

Muchas gracias por comentar!

Arturo dijo...

Mi problema era que IdentityServer estaba intentando buscar un archivo que se crea como llave para un token precisamente en la carpeta de IIS a la cual no tenía acceso. Quitándole esa configuración ya pude publicar la aplicación en proceso. Gracias!

Arturo dijo...

No me tronaba en local porque estaba utilizando dotnet run, pues eso no es montarlo con IIS, ese fue mi error.

José María Aguilar dijo...

Genial :)

Muchas gracias por comentarlo, puede ser útil para alguien más.

Un saludo!

sanmolhec dijo...

En Core 3 sigue siendo necesario especificar el AspNetCoreHostingModel??

José María Aguilar dijo...

Hola Héctor!

En 3.x el valor por defecto es in-process, por lo que normalmente no habrá que indicar nada salvo que quieras establecerlo en out-of-process.

Un saludo!

Héctor dijo...

Se me pasó darte las gracias por la respuesta. Pues más fácil para todos entonces.

Saludos, Héctor.