Ya habíamos hablado sobre cómo hacer uso del objeto Cache de ASP.NET para poder guardar en caché aquellos objetos que almacenan datos que requieren de un largo tiempo de procesado para ser obtenidos y que no cambian con demasiada frecuéncia.  Con el uso del objeto Cache, evitábamos tener que volver a recuperar los datos (con todo lo que eso implica: realizar la conexión a la base de datos, realizar la consulta, cerrar la conexión y realizar un tratamiento de los datos si es necesario). Eso repercute en una mejora sustancial en el tiempo de procesado de una petición a un recurso determinado. Pero ¿cómo podemos reducir aún más ese tiempo?  Hasta ahora, guardamos en caché el resultado de consultas a una base de datos, a un archivo o cualquier objeto cuya inicialización u obtención requiere de un tiempo notable de procesado. Ahora bien, eso no quita que se deba llevar a cabo toda la renderización de la página pasando por cada uno de los eventos de ASP.NET (PreInit, Init, Load, PreRender, Render, etc…) para acabar generando el código Html que se envia finalmente al cliente. Si prestamos atención, veremos que el simple hecho de renderizar los controles de servidor (WebControls) a tags Html implica un coste elevado (pero necesario) de tiempo. Necesario si pero ¿siempre?. La respuesta es no. Si analizamos con calma las páginas de nuestro site, nos daremos cuenta de que muchas de ellas son completamente estáticas (siempre muestran el mismo contenido) y que otras disponen de contenidos dinámicos que cambian cada cierto tiempo (pero no de forma contínua, ni por usuario ni nada por el estilo).
Si lo miramos así, sería genial poder guardar en Cache el resultado de la renderización de estás páginas (es decir, Cachear el códgio html que se envia al cliente) evitando así todo el proceso de renderización de la página. Es aquí donde entra en escena la directiva OutputCache.

Escenario:

Pongámonos en situación. Con tal de demostrar el uso de la directiva OutputCache, desarrollaremos una pequeña aplicación web en la que se mezclen formularios web estáticos y dinámicos (como ocurre en el 90% de las aplicaciones web). La aplicación constará de una página inicial en la que se mostrará un menú mediante el cual el usuario navegará por el web site.  Crearemos un formulario web en el que el usuario especificará el nombre de un Artista y se mostrarán los Álbumes editados por dicho Artista. Por último veremos ejemplos de cómo mejorar ciertas partes del Caching mediante el uso de UserControls.

OutputCache: la directiva

La directiva OutputCache debe aplicarse al código ASP.NET o html de nuestra página.  La sintaxi para utilizar esta directiva es la siguiente:

<%@ OutputCache Duration=“x” VaryByParam=“none” %/>

Como puede observarse, además de declarar la directiva, deben especificarse como mínimo dos atributos:

  • Duration: especifica el tiempo que se almacena en cache el resultado de esta página
  • VaryByParam: especifica si se debe guardar en cache una versión por cada valor de un parámetro concreto. Por ejemplo, podríamos indicar VaryByParam=”name” y se guardaría una versión de la página por cada uno de los posibles valores de la variable “name”. Si hacemos una llamada a la página con name=”javi” se guardará el resultado en cache y si luego hacemos la misma llamada pero con name=”luis” se guardará otra versión de la misma página pero con el resultado cuando la variable name=”luis”.

La directiva acepta otros atributos que iremos comentando más adelante, pero estos dos son los mínimos requeridos.
Como veremos más adelante, podremos configurar todos estos elementos tanto en código asp.net html como en el archivo web.config.

Así pues, la primera imagen que se muestra es la página principal del site:

Página principal del site

Página principal del site

Como puede verse, la página consta de un menú de navegación y varios textos estáticos. Esta página es una firme candidata a ser “cacheada”, puesto que no parece probable que vaya a ser modificada de forma asidua y sus controles tienen datos estáticos. Si miramos el trace generado por la petición para obtener esta página, veremos lo siguiente:

Trace de la petición inicial

Trace de la petición inicial

El tiempo total en renderizar la página es de 0.0134 segundos. Como ya hemos dicho, esta página es la candidata ideal para ser “cacheada”. Para ello, lo que haremos será añadir la directiva OutputCache a la página, tal y como se muestra en la siguiente imagen:

Añadimos la directiva OutputCache con los atributos requeridos

Añadimos la directiva OutputCache con los atributos requeridos

El resultado de añadir esta directiva es que la página no volverá a ser renderizada (atención, nos saltaremos TODO el proceso de renderización de la página, con lo que el ahorro de tiempo es máximo) y se devolverá inmediatamente el resultado al cliente. Si ahora volvemos a ejecutar la página y miramos el trace, encontraremos lo siguiente:

Trace con la directiva OutputCache activada.

Trace con la directiva OutputCache activada.

Como podéis observar, ni siquiera marca entrada alguna en el registro de tiempos. Eso se debe a que se ha saltado todo el proceso de renderización de la página, con lo que no se han podido calcular los tiempos invertidos en cada fase o evento del proceso. Si, el ahorro de tiempo es máximo.

Imaginemos ahora que, en esta misma página, tan sólo hay un elemento texto que debe ser dinámico. Pongamos, por ejemplo, que queremos que se muestre la hora del servidor en nuestra web. Claro, si cacheamos la página entera, siempre aparecerá la misma hora (hasta que caduque la caché). ¿Significa eso que ya no puedo guardar en Cache esta página? Pues no, ni mucho menos. Para este caso en concreto, en el que deseamos que cierto texto sea dinámico, podemos utilizar el control Substitution. Este control nos permite sustituir determinados valores de nuestra página en Cache, devolviendo así siempre el valor deseado y manteniendo en Caché la página en sí.
Vamos a mostrar un ejemplo para que quede demostrado el uso del control Substitution:

Añadiremos, al pie de nuestra web, dos textos en los que se indicará la hora del servidor. El primero se ejecutará desde el lado del servidor, es decir, obtendremos el valor del tiempo en el evento On_Load de la página y lo mostraremos. El segundo utilizará el control Substitution para mostrar la hora:

Muestra cómo queda el diseño de la página. Además se muestra el control Substitution en la caja de herramientas.

Muestra cómo queda el diseño de la página. Además se muestra el control Substitution en la caja de herramientas.

Para que el control Substitution funcione correctamente, es necesario especificar, en sus propiedades, el método al que debe llamar para obetener el texto a mostrar.  El código de la página principal se muestra en la siguiente imagen:

Se recoge la fecha del servidor en el evento Load de la página y en el método llamado por el control Substitution

Se recoge la fecha del servidor en el evento Load de la página y en el método llamado por el control Substitution

Ahora podemos ejecutar la aplicación. Al recibir la primera petición, ambos controles muestran el mismo valor:

Ambas lineas muestran el mismo valor al recibir la primera petición (ya que la página no está cacheada)

Ambas lineas muestran el mismo valor al recibir la primera petición (ya que la página no está cacheada)

Una vez se ha recibido la respuesta, el servidor a cacheado la página, con lo que si ahora refrescamos, como la duración es de 60 segundos, el servidor nos devolverá la página cacheada, pero aplicando la sustitución al control substitution, por lo que el resultado es este:

El tiempo mostrado por el control Substitution se ha refrescado, mientras que el otro no, ya que proviene de la página Cacheada.

El tiempo mostrado por el control Substitution se ha refrescado, mientras que el otro no, ya que proviene de la página Cacheada.

Como puede verse, la primera linea mantiene el valor que hay en la página Cacheada mientras que el valor de la segunda linea se ha actualizado gracias al uso del control Substitution, mostrando así la hora del servidor actualizada.

Pero bien, no nos vamos a ganar el cielo tan fácilmente no? Claro…y que ocurre con aquellas páginas cuyos datos són dinámicos (bien porque contienen controles dinamicos como listview, gridview, formview, detailsview,etc… o bien porque las construimos de forma dinámica).
Bueno, lo primero que hay que comentar es que no todo puede ser cacheado. Utilizando la Cache ganaremos en velocidad (mejor dicho, tiempo de respuesta), pero la contrapartida es que aumentaremos la memoria ocupada y esto, cuando llega a ciertos límites, acaba produciendo una disminución de la velocidad y finalmente un colapso en el servidor. Debemos elegir cuidadosamente aquellas páginas/elementos a cachear.

Segundo: debemos elegir aquellas páginas que varien por parámetros cuyos rangos de valores no sean demasiado grandes, pues por cada uno de los valores comprendidos en dicho rango se almacenaría una cópia de la página.

Bien, veamos qué podemos hacer con una página que contiene datos dinámicos.

La página que trataremos de “agilizar” es la que se muestra en las siguientes imágenes:

El usuario selecciona un Artista y se le muestran todos los Álbumes de que se dispone.

El usuario selecciona un Artista y se le muestran todos los Álbumes de que se dispone.

Otro ejemplo, con un Artista diferente

Otro ejemplo, con un Artista diferente

Bien, podemos observar que esta página contiene datos estáticos (la parte superior de la misma, en la que se muestra el título, el menú, etc..) y datos dinámicos (el DropDownList + el GridView). Evidentemente no podemos guardar en cache una versión de la página por cada uno de los Artistas que hay en el DropDownList (ya que la lista es interminable), Cachear la página tal y como hicimos en el ejemplo anterior no tiene sentido, pues siempre veríamos el mismo album y artista. Entonces ¿está página no puede cachearse? La respuesta es no tal y como está. El resultado del DropDownList y del GridView no podremos cachearlos, pues son dinámicos (en cualquier caso podríamos cachear el datasource del dropDownList y, quizás el del GridView con tal de no volver a buscar en la base de datos). Pero fijémonos bien. Hay una parte en la página que es estática. Podríamos reducir el tiempo necesario para renderizar esta página si pudiéramos cachear sólamente esa parte. Pues lo cierto es que podemos hacerlo. El tema consiste en poner toda esa parte estática dentro de un UserControl y cachear ese UserControl tal y como hicimos en el ejemplo anterior.

UserControl que contiene la cabecera estática

UserControl que contiene la cabecera estática

Ahora, añadimos la cabecera tanto a la página principal, como a la página de búsqueda de albums:

Añadimos la cabecera al Webform de búsqueda de Albums

Añadimos la cabecera al Webform de búsqueda de Albums

Antes de marcar el UserControl para ser cacheado, vamos a comprobar el tiempo que tarda la petición en el servidor:

Muestra el trace inicial (sin outputCache del UserControl)

Muestra el trace inicial (sin outputCache del UserControl)

Ahora añadiremos la directiva OutputCache a nuestro UserControl, tal y como se muestra en la siguiente imagen:

Añadimos la directiva OutputCache al UserControl de la misma forma que hicimos con la página principal.

Añadimos la directiva OutputCache al UserControl de la misma forma que hicimos con la página principal.

Y finalmente volveremos a realizar la petición para volver a comprobar el tiempo que se tarda en generar la respuesta a la petición:

Se puede comprobar la disminución del tiempo necesaria para generar la respuesta a la petición

Se puede comprobar la disminución del tiempo necesario para generar la respuesta a la petición

Se puede apreciar la diferencia del tiempo invertido en generar la misma respuesta que en la petición anterior.

Configurando la directiva OutputCache:

Hasta el momento sólo hemos utilizado dos atributos de la directiva OutputCache: Duration y VaryByParam. Pero la directiva admite otros atributos que nos pueden proporcionar funcionalidades muy interesantes:

  • Shared: determina si se puede compartir la versión cacheada con múltiples páginas.
  • SqlDependency: al igual que con el objeto Cache, podemos crear una dependencia a una tabla de una base de datos concreta.
  • VaryByCustom: Nos permite especificar si queremos guardar una versión de la página por cada Browser y versión o bien por cada valor diferente de un string que le pasamos.
  • VaryByHeader: Guarda una versión por cada valor diferente de una cabecera http determinada.
  • NoStore: Se envia la cabecera no-store. No disponible para UserControls.
  • Location: especifica las cabeceras de cache que se van a enviar al cliente. De esta forma se determina si la versión de la página se almacena en el cliente, en el servidor, en un proxy, etc…

El objeto HttpCachePolicy:

Una forma alternativa de controlar el OutputCache es mediante el objeto HttpCachePolicy del cual obtenemos unas instancia a través del objeto Response. Con este objeto podemos configurar, programaticamente lo mismo que hemos configurado mediante la directiva OutputCache, aunque nos ofrece algunas funcionalidades extras, como por ejemplo el SlidingExpiration o la renovación del timeout (si una página cacheada tiene una duración de 20 segundos y en el segundo 19 alguien realiza una petición, la duración vuelve a renovarse 20 segundos más).

Para recuperar el objeto, como ya he comentado antes, lo haremos a través del objeto Response tal que así:

El objeto HttpCachePolicy

El objeto HttpCachePolicy

el nombre Cache no es demasiado adecuado en este contexto, ya que podría confundirse con el objeto Cache que tratamos en el artículo anterior. Por ese motivo debemos tener claros los conceptos.

Cache Dependency:

Al igual que ocurre con el objeto Cache, podemos añadir dependencias a la directiva OutputCache, de forma que podamos caducar la cache en función de la dependencia/s que le hemos añadido.

Para añadir una dependencia a la directiva OutputCache, tan solo deberemos utilizar el objeto Response y su método AddCacheDependency como se muestra en la siguiente imagen:

Añadimos una dependencia a archivo a la directiva OutputCache

Añadimos una dependencia a archivo a la directiva OutputCache

En cambio, si queremos añadir una dependencia a una tabla de SqlServer debemos modificar la directiva añadiéndole el atributo SqlDependency.

Modificamos la directiva para que acepte dependencias a Sql

Modificamos la directiva para que acepte dependencias a Sql

Este atributo acepta dos tipos de valores diferentes:

  • CommandNotification: si queremos utilizar la notificación de sentencia de SQL Server 2005 y posteriores (no disponible si aplicamos la directiva a UserControls).
  • String con DatabaseName/Table: un string que define el nombre de la base de datos y la tabla a la que se añade la dependencia.

Finalmente, añadiremos la dependencia mediante el objeto Response, como en el ejemplo anterior.
Cabe recordar que para dependencias SQL es necesario añadir las correspondientes entradas en el archivo de configuración web.config (tal y como se comentó en el artículo anterior).

Configuramos la dependencia Sql en el archivo web.config (o en el especificado en el atributo configSource)

Configuramos la dependencia Sql en el archivo web.config (o en el especificado en el atributo configSource)

OutputCacheProfiles:

Hasta el momento, cualquier configuración sobre la directiva OutputCache la hemos realizado directamente en el código de nuestra página aspx. En muchas ocasiones nos gustaría tener la ibertad de poder modificar, en tiempo de ejecución, los parámetros de dicha directiva (por ejemplo, queremos cambiar el atributo Duration para que la Cache caduque antes o más tarde). En estos casos podemos utilizar (y de hecho es una práctica muy recomendable) los archivos de configuración de la aplicación web para almacenar lo que se conoce como OutputCache profiles.
Estos profiles no son más que los mismos atributos que configuramos en la directiva OutputCache con la ventaja de poder modificarlos directamente en el archivo web.config (o en otro archivo xml siempre que se configure el atributo configSource del nodo correctamente).

Para añadir un nuevo profile al archivo de configuración, solo es necesario crear la siguiente estructura xml:

Ejemplo de configuración de un profile de OutputCache

Ejemplo de configuración de un profile de OutputCache

El archivo web.config en VS2005 y 2008 dispone de IntelliSense, por lo que no es necesario recordar cada unos de los atributos posibles. Es suficiente con seleccionarlos de la lista.
Podemos añadir tantos profiles como queramos. Una vez añadido el profile, sólo nos queda indicarle a nuestra página aspx que debe utilizar dicho profile para configurar la directiva OutputCache. Esto lo conseguimos modificando la directiva de la siguiente forma:

<%@ OutputCache CacheProfile=”MiConfiguracion”%>

Con este simple cambio, nuestra directiva leerá su configuración del archivo de configuración de la aplicación web.

Y eso es todo. Creo que se han abordado ámpliamente todos los aspectos relacionados con la directiva outputcache, dando una visión global bastante completa. Espero que os sirva en vuestros proyectos.

Un saludo!