martes, 8 de noviembre de 2011

Ciclo de vida de una petición ASP.NET

Para cualquier desarrollador de ASP.NET es muy importante poder extender la funcionalidad que nos ofrece de serie el ciclo de vida estándar de una petición.

Llega un momento en el que no podemos realizar ciertas tareas sólo agregando formularios web a nuestra aplicación. Es justo en ese momento cuando es necesario conocer el ciclo de vida de una petición en ASP.NET, para extender de forma eficiente el comportamiento predeterminado y codificar con éxito nuestros requerimientos.

El ciclo de vida de una petición en ASP.NET es gestionado por módulos y manejadores.

Cualquier petición ASP.NET sigue el siguiente flujo (pipeline):

clip_image001

  • La petición procesa todos los módulos que están suscritos a los eventos de entrada.
  • La petición es procesada por un único manejador, que es responsable de generar el marcado devuelto al cliente.
  • La petición vuelve a procesar todos los módulos suscritos a los eventos de salida.

Un módulo es una clase que implementa la interface System.Web.IHttpModule y que se suscribe a eventos expuestos por la aplicación o petición.

Si el evento al que está suscrito nuestro módulo es lanzado durante el ciclo de vida de la petición, nuestro código personalizado será llamado y tendremos la oportunidad de realizar las tareas que estimemos oportunas con la petición actual.

Los eventos disponibles a los que puede suscribirse un módulo son los que expone la clase System.Web.HttpApplication.

Estos eventos permiten incluir código personalizado en distintas fases del ciclo de vida de la petición, así por ejemplo el evento BeginRequest es siempre el primero evento que sucede durante la petición y el evento Error es lanzado cuando se produce una excepción no manejada.

Cómo la suerte del programador es elegir que evento concreto queremos capturar, a continuación expongo la lista de eventos completa.

AcquireRequestState

Se produce cuando ASP.NET adquiere el estado actual (por ejemplo, estado de sesión) asociado a la solicitud actual.

AuthenticateRequest

Se produce cuando un módulo de seguridad ha establecido la identidad del usuario

AuthorizeRequest

Se produce cuando un módulo de seguridad ha comprobado la autorización del usuario

BeginRequest

Se produce como el primer evento de la cadena de ejecución de canalización HTTP cuando ASP.NET responde a una solicitud.

Disposed

Se produce cuando se desecha la aplicación.

EndRequest

Se produce como el último evento de la cadena de ejecución de canalización HTTP cuando ASP.NET responde a una solicitud.

Error

Se produce cuando se produce una excepción no controlada.

LogRequest

Se produce justo antes de que ASP.NET realice cualquier registro para la solicitud actual.

MapRequestHandler

Infraestructura. Se produce cuando se selecciona el controlador para responder a la solicitud.

PostAcquireRequestState

Se produce cuando se ha obtenido el estado de solicitud (por ejemplo, el estado de sesión) asociado con la solicitud actual.

PostAuthenticateRequest

Se produce cuando un módulo de seguridad ha establecido la identidad del usuario.

PostAuthorizeRequest

Se produce cuando se ha autorizado al usuario de la solicitud actual.

PostLogRequest

Se produce cuando ASP.NET ha completado el procesamiento de todos los controladores de eventos para el evento LogRequest.

PostMapRequestHandler

Se produce cuando ASP.NET ha asignado la solicitud actual al controlador de eventos adecuado.

PostReleaseRequestState

Se produce cuando ASP.NET ha finalizado de ejecutar todos los controladores de eventos de la solicitud y se han almacenado los datos de estado de la solicitud.

PostRequestHandlerExecute

Se produce cuando el controlador de eventos ASP.NET (por ejemplo, una página o un servicio Web XML) finaliza su ejecución.

PostResolveRequestCache

Se produce cuando ASP.NET omite la ejecución del controlador de eventos actual y permite que un módulo de almacenamiento en caché atienda una solicitud desde la caché.

PostUpdateRequestCache

Se produce cuando ASP.NET completa la actualización de los módulos de almacenamiento en caché que almacenan las respuestas que se usan para atender las posteriores solicitudes de la caché.

PreRequestHandlerExecute

Se produce justo antes de que ASP.NET comience a ejecutar un controlador de eventos (por ejemplo, una página o un servicio Web XML).

PreSendRequestContent

Se produce inmediatamente antes de que ASP.NET envíe el contenido al cliente.

PreSendRequestHeaders

Se produce inmediatamente antes de que ASP.NET envíe los encabezados HTTP al cliente.

ReleaseRequestState

Se produce después de que ASP.NET termine de ejecutar todos los controladores de eventos de la solicitud. Este evento permite que los módulos de estado guarden los datos de estado actuales.

ResolveRequestCache

Se produce cuando ASP.NET finaliza un evento de autorización para permitir que los módulos de almacenamiento en caché atiendan las solicitudes de la caché, evitando la ejecución del controlador de eventos (por ejemplo, una página o servicio Web XML).

UpdateRequestCache

Se produce cuando ASP.NET finaliza la ejecución de un controlador de eventos para permitir que los módulos de almacenamiento en caché almacenen las respuestas que se van a utilizar para atender las solicitudes siguientes de la caché.

 

Al respecto de esta lista de eventos cabe mencionar que no todos ellos tienen porque ser lanzados durante una petición (por ejemplo, dios quiera que no haya siempre errores no manejados por cada petición).

En cualquier caso, para un sitio web vacío recién creado y agregando un formulario web (WebForm) sin ningún contenido ni código extra, se lanzarán los siguientes eventos en el orden expuesto:

  • BeginRequest
  • AuthenticateRequest
  • PostAuthenticateRequest
  • AuthorizeRequest
  • PostAuthorizeRequest
  • ResolveRequestCache
  • PostResolveRequestCache
  • MapRequestHandler
  • PostMapRequestHandler
  • AcquireRequestState
  • PostAcquireRequestState
  • PreRequestHandlerExecute
  • PostRequestHandlerExecute
  • ReleaseRequestState
  • PostReleaseRequestState
  • UpdateRequestCache
  • PostUpdateRequestCache
  • LogRequest
  • PostLogRequest
  • EndRequest
  • PreSendRequestContent
  • PreSendRequestHeaders

En realidad, también es posible capturar todos estos eventos sin crear ninguna clase personalizada y esto es porque tenemos disponible el famoso fichero Global.asax que crea por nosotros una clase llamada global_asax que hereda de System.Web.HttpApplication y nos permite incluir código en el pipeline sin crear un nuevo módulo. Bajo cuerda lo que pasa es que se buscan métodos con un cierto nombre por reflexión para ser ejecutados automáticamente (más info en http://stackoverflow.com/questions/10612351/how-global-asax-works/10612500#10612500). Por ejemplo, si queremos captura BeginRequest tendremos que escribir un método con la firma protected void Application_BeginRequest(object sender, EventArgs e). A este respecto hay que tener en cuenta que los eventos capturados en el Global.asax sucederán después de sus eventos homónimos codificados en módulos.

Aunque tengamos disponible el fichero Global.asax, el acercamiento a través de módulos tiene ciertas ventajas como que podemos separar nuestro código en un nuevo ensamblado y registrar el módulo a través del fichero de configuración de la aplicación (web.config). De este modo, podemos agregar funcionalidad a nuestra aplicación sin tener que tocar el código de la misma y evitarnos así volver a compilar nuestro proyecto.

Por otro lado, si utilizamos el fichero Global.asax, disfrutaremos de ciertos eventos que están sólo disponibles a través de este fichero:

·         Application_OnStart

·         Application_OnEnd

·         Session_OnStart

·         Session_OnEnd

Para ilustrar todo lo visto con un ejemplo concreto e implementar nuestro propio módulo, imaginemos que a través de un setting decidiremos si nuestra aplicación es sólo accesible desde el servidor donde está alojada.

Los pasos a seguir son los siguientes:

Agregar un setting de aplicación con nuestro compartamiento:

  <appSettings>

    <add key="OnlyLocal" value="True"/>

  </appSettings>


Crear una biblioteca de clases llamada “CicloVidaASPNETPersonalizado” y agregar una referencia al ensamblado System.Web.

Agregar una clase llamada “ModuloPersonalizado” que implementará la interface System.Web.IHttpModule, con el siguiente código:

 

Imports System.Web

Imports System.Web.Configuration

 

Public Class ModuloPersonalizado

    Implements IHttpModule

 

    Public Sub Dispose() Implements IHttpModule.Dispose

 

    End Sub

 

    Public Sub Init(context As System.Web.HttpApplication) Implements IHttpModule.Init

        AddHandler context.BeginRequest, AddressOf BeginRequest

    End Sub

 

    Protected Sub BeginRequest(sender As Object, e As System.EventArgs)

        Dim app As HttpApplication = CType(sender, HttpApplication)

        Dim onlyLocal As Boolean = Convert.ToBoolean(WebConfigurationManager.AppSettings.Item("OnlyLocal"))

        If onlyLocal AndAlso Not app.Request.IsLocal Then

            app.Response.Write("No tiene acceso a la aplicación")

            app.Response.End()

        End If

    End Sub

 

End Class


Nuestro último paso será registrar el modulo a través del fichero de configuración del sitio web. Dependiendo de sí desplegamos nuestra aplicación en IIS 6.0/IIS 7.0 en modo clásico o IIS 7.0 en modo integrado (predeterminado), el código para registrar el módulo es distinto. En la práctica, se suele registrar para ambos entornos y así no hay problema durante el despliegue. En cualquier caso, lo que haremos será especificar el nombre completo de nuestra clase, cuyo ensamblado podría estar tanto en el directorio \bin de nuestro sitio web (despliegue privado), como en el GAC.

<?xml version="1.0"?>

<configuration>

  <appSettings>

    <add key="OnlyLocal" value="True"/>

  </appSettings>

  <system.web>

    <compilation debug="false" strict="false" explicit="true" targetFramework="4.0"/>

    <!--httpModules para IIS 6.0 / 7.0 en modo clásico-->

    <httpModules>

      <add type="CicloVidaASPNETPersonalizado.ModuloPersonalizado" name="MiModulo"  />

    </httpModules>

  </system.web>

  <system.webServer>

    <!--modules para IIS 7.0 en modo integrado-->

    <modules>

      <add type="CicloVidaASPNETPersonalizado.ModuloPersonalizado" name="MiModulo"/>

    </modules>

  </system.webServer>

</configuration>


Aunque ejemplo no es muy definitivo en lo relativo a su funcionalidad, espero que te haya servido para ver cómo encaja un módulo en pipeline de ASP.NET.

De hecho, el propio ASP.NET registra diversos módulos para ofrecernos muchas de sus características. Alguno de estos módulos que seguro identificaras con funcionalidades básicas de ASP.NET podrían ser:

<add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />

<add name="Session" type="System.Web.SessionState.SessionStateModule" />

<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />

<add name="RoleManager" type="System.Web.Security.RoleManagerModule" />


Si estamos en un IIS 7, también podemos configurar nuestros módulos directamente desde el administrador de Internet Information Services:

clip_image003

Una vez que ya hemos visto los módulos, ahora resta por ver dónde y cómo actúan los manejadores en el ciclo de vida de una petición.

Como dijimos al principio, cualquier petición ASP.NET es finalmente procesada por un único manejador. Siendo así, me parece adecuado ver primero como se registran los manejadores y después hacer un ejemplo concreto.

Un manejador se registra desde nuestro fichero de configuración a través de la sección httpHandlers (si IIS 6.0) o handlers (si II 7.0). Es decir, de nuevo utilizaremos tanto la sección system.web como la sección system.webServer.

Un manejador se registra con la instrucción add (que he simplificado deliberadamente para nuestro post):

<add verb="verb list"

     path="path/wildcard"

     type="type,assemblyname" />

 

verb

Indica una lista de verbos HTTP para los que será efectivo este manejador.

Permite el carácter *.

Por ejemplo: GET,POST o *.

path

Indica para que rutas será efectivo este manejador.

Permite el carácter *.

Por ejemplo: *.aspx

type

Especifica la clase y ensamblado desde donde cargar la clase que implementa el manejador.


Si vemos algunos de los manejadores que automáticamente registra ASP.NET, entenderemos su uso mucho mejor:

<add path="trace.axd" verb="*" type="System.Web.Handlers.TraceHandler" validate="True" />

<add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="True" />

<add path="*.config" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />


Para el primer ejemplo, una petición a la url concreta trace.axd será servida por la clase System.Web.Handlers.TraceHandler.

En el segundo ejemplo podemos ver como todos los ficheros .aspx (sin importar su ruta) son procesados por System.Web.UI.PageHandlerFactory.

Finalmente, cualquier fichero .config no será servido porque lo procesa el tipo System.Web.HttpForbiddenHandler que impide que se devuelva su contenido (en realidad se devolverá el código de estado HTTP 403).

Como te puedes imaginar, hay muchísimos registros de manejadores que ASP.NET incluye de serie, y los aquí mostrados son sólo unos pocos.

Ahora y para ver un ejemplo de cómo podemos crear nuestro propio manejador, pensemos que queremos procesar un nuevo tipo de fichero llamado .panicoenlaxbox.

Para crear un manejador personalizado y registrarlo, serán necesarios los siguientes pasos:

  • Crear una clase que implemente la interface IHttpHandler.
  • Si necesitamos acceder a la sesión del usuario, implementar la interface de marcado (no requiere código) IRequiresSessionState o IReadOnlySessionState (si el acceso sólo será de lectura).
  • Registrar nuestro manejador en el fichero web.config.
  • Si estamos en IIS 6.0, registrar el manejador desde el administrador de Internet Information Services.

El código de nuestro manejador será el siguiente:

Public Class ManejadorPersonalizado

    Implements IHttpHandler

 

    Public ReadOnly Property IsReusable As Boolean Implements System.Web.IHttpHandler.IsReusable

        Get

            Return False

        End Get

    End Property

 

    Public Sub ProcessRequest(context As System.Web.HttpContext) Implements System.Web.IHttpHandler.ProcessRequest

        context.Response.ContentType = "text/plain"

        context.Response.Write("Hola desde panicoenlaxbox.blogspot.com")

        context.Response.End()

    End Sub

End Class


Para registrarlo en nuestro fichero web.config, agregaremos el siguiente código:

<?xml version="1.0"?>

<configuration>

  <appSettings>

    <add key="OnlyLocal" value="True"/>

  </appSettings>

  <system.web>

    <compilation debug="true" strict="false" explicit="true" targetFramework="4.0"/>

    <!--httpModules para IIS 6.0 / 7.0 en modo clásico-->

    <httpModules>

      <add type="CicloVidaASPNETPersonalizado.ModuloPersonalizado" name="MiModulo"/>

    </httpModules>

    <httpHandlers>

      <add verb="*" path="*.panicoenlaxbox" type="CicloVidaASPNETPersonalizado.ManejadorPersonalizado" />

    </httpHandlers>

  </system.web>

  <system.webServer>

    <!--modules para IIS 7.0 en modo integrado-->

    <modules>

      <add name="CicloVidaASPNETPersonalizado.ModuloPersonalizado" type="MiModulo"/>

    </modules>

    <handlers>

      <add verb="*" path="*.panicoenlaxbox" type="CicloVidaASPNETPersonalizado.ManejadorPersonalizado" name="MiManejador" />

    </handlers>

  </system.webServer>

</configuration>


Si además estamos desplegando nuestra aplicación en IIS 6.0, necesitaremos registrar el nuevo tipo de extensión desde el administrador de Internet Information Services.

clip_image005

Fijarse en que está desmarcado “Comprobar si el archivo existe”.

Actualización 10/07/2014. Todo lo aquí explicado también es válido para proyectos de ASP.NET MVC o WebApi, sin embargo en ese tipo de proyectos tendremos que agregar una ruta a ignorar en routes.IgnoreRoute, sino la tabla de rutas de MVC se adueñara de la ruta y no la procesará. Aunque estarías tentando de escribir routes.IgnoreRoute("{resource}.panicoenlaxbox/{*pathInfo}"); más que nada por imitar la ruta que viene ignorada de serie para los ficheros .axd, nada más lejos de la realidad. Parece que la sintaxis de IgnoreRoute es algo “oscura” y lo correcto sería algo así: routes.IgnoreRoute("{*allpanicoenlaxbox}", new { allpanicoenlaxbox = @".*\.panicoenlaxbox(/.*)?" }); Mas información en http://haacked.com/archive/2008/07/14/make-routing-ignore-requests-for-a-file-extension.aspx/

Lo último que querría decir al respecto de los manejadores, es que también existe la posibilidad de crear lo que se denominan “manejadores genéricos”, que son ficheros con la extensión .ashx que una vez comienzas a utilizarlos te das cuenta de que tienen grandes posibilidades y permiten una vía de escape a los encorsetados WebForms. El concepto detrás de estos manejadores es el mismo que en sus homónimos, pero esta vez su utilización será directamente llamándolos a través de una url, por ejemplo: panicoenlaxbox/saludar.ashx.

clip_image007

Puedes visitar el siguiente enlace para ver un ejemplo completo de un controlador genérico.

La verdad es que este post podría ser infinito, pero yo me doy por satisfecho y espero que tú también.

Un saludo!

No hay comentarios:

Publicar un comentario