No artigo anterior, falei sobre o Application Caching, mecanismo capaz de armazenar objetos na memória do servidor para otimizar o acesso de novas requisições. Neste artigo irei abordar a segunda formar de caching disponibilizada pelo Asp.Net, o Page Output Caching.

Neste tipo de caching páginas ou seções dela são armazenadas na memória, com isso, novas solicitações não terão que realizar todo processamento para geração da página, o servidor simplesmente envia o HTML armazenado para o cliente que efetuou a requisição.

1. Passo a passo de uma requisição com Asp.Net

Para explicitar os benefícios do caching em páginas Asp.Net será mostrada as etapas de uma requisição web padrão, e compará-las com as etapas de uma requisição a uma página que já está em cache.

A imagem abaixo demonstra os eventos que ocorrem desde a solicitação do usuário no browser até a resposta do servidor, para uma página que não está em cache.

Passo a passo de uma requisição sem utilização do Output Caching

Figura 1: Passo a passo de uma requisição sem utilização do Output Caching.

O processo começa com a requisição do cliente. Ao chegar ao servidor, a página é encontrada e enviada ao Parse do Asp.Net, onde o código fonte será interpretado gerando instruções que serão enviadas para o compilador. Este é responsável por gerar o Assembly da página, que depois de criado é enviado para a memória do Runtime HTTP, onde as instruções serão executadas gerando a página solicitada. Quando uma segunda solicitação é realizada, as etapas de interpretação e compilação são puladas, pois o Assembly já está criado no servidor.

Agora imagine uma aplicação web com inúmeros clientes, cada cliente requisitando a mesma página que demanda um considerável processamento para sua criação, mas que não muda com tanta frequência. Para cada requisição todo o processamento visto anteriormente será necessário, se houverem 1000 requisições, 1000 vezes será executado, mesmo se o conteúdo não mudar.

A próxima imagem demonstra uma requisição a uma página que está no cache. Podemos notar a economia de recursos do servidor ao utilizar o Output Caching. Após a primeira requisição o conteúdo será armazenado em cache e todas as solicitações subsequentes receberão apenas o conteúdo HTML que já está armazenado, não sendo necessário o processamento da página.

Passo a passo de uma requisição com utilização do Output Caching

Figura 2: Passo a passo de uma requisição com utilização do Output Caching.

2. Ativando o caching de página

Para ativar o mecanismo de caching em páginas .aspx é necessário informar no topo da página a diretiva @OutputCache da seguinte forma:

Listagem 1: Informando diretiva @OutputCache no Topo da Página

<%@ OutputCache ... %>

Essa diretiva possui vários parâmetros e de acordo com suas configurações vários resultados e comportamentos podem ser obtidos. As seções seguintes mostram como obter esses diferentes resultados.

2.1. Definindo o tempo de duração do caching

Para definir o tempo em que o cache de uma página é válido basta informar um valor inteiro para o parâmetro Duration, o valor informado irá corresponder ao tempo em segundos que a página estará em cache. Este é um parâmetro obrigatório, se não informado irá gerar uma exceção em runtime. O exemplo adiante ativa o caching da página por um tempo de trinta segundos.

Listagem 2: Definindo tempo de validade do caching

<%@ OutputCache Duration="30" VaryByParam="none"%>

2.2. Definindo a localização do caching

Outra configuração possível é informar onde o caching será armazenado. Configurando a propriedade Location é possível informar se o caching estará no cliente, no servidor ou em servidores proxy que participarem da solicitação. Os seguintes valores são possíveis:

  • Any: O caching pode ser armazenado em qualquer mecanismo participante da requisição. Browser, servidores de proxy e o próprio servidor web;
  • Client: Armazenado apenas no browser do cliente onde a solicitação foi originada;
  • Downstream: Armazenado em qualquer dispositivo HTTP 1.1 que possui capacidade de cache e que participou da requisição, incluindo o cliente;
  • Server: O caching estará armazenado apenas no servidor web que processou a requisição;
  • None: Desabilita o caching;
  • ServerAndClient: Armazena o caching apenas no servidor que processou a requisição e no cliente que a realizou, não permitindo o armazenamento em servidores proxy que por ventura tenham participado do processo.

O exemplo abaixo mostra como ativar o caching com duração de 10 segundos e armazená-lo apenas no servidor web.

Listagem 3: Ativando caching com duração de 10 segundos

<%@ OutputCache Duration="10" VaryByParam="none" Location="Server"%>

2.3. Parâmetro NoStore

A diretiva OutputCache possui ainda o parâmetro NoStore, que serve para informar aos browsers e servidores proxys, participantes da requisição, que não devem armazenar uma cópia permanente da página. É geralmente utilizado em páginas que possuem conteúdo sensível, como páginas de login. Configurado da seguinte maneira:

Listagem 4: Configurando OutputCache

<%OutputCache Duration="30" VaryByParam="none" NoStore=”true” %>

2.4. Armazenamento em cache de acordo com a QueryString

Existem cenários em que as páginas devem mudar seu conteúdo de acordo com parâmetros passados pela QueryString, mas apesar da mudança, o conteúdo específico de cada página/parâmetro não muda com frequência. Para esses cenários é possível utilizar o caching configurando o parâmetro VaryByParam, que consiste em uma lista de strings separadas por ponto-vírgula, indicando os parâmetros passados pela QueryString que devem ser considerados no cache. Observe o exemplo a seguir:

Listagem 5: Cache variável por parâmetro

<%@ OutputCache Duration="3600" VaryByParam="Id" %>

Neste exemplo acima, o caching foi ativado levando em consideração o parâmetro “Id”, passado via QueryString. Quando o servidor receber solicitações de uma página informando esse parâmetro, para cada valor passado será armazenada uma página no caching. Para cada uma das requisições abaixo o resultado da página irá ser armazenado no cache:

Listagem 6: Requisições para altrar o cache por parâmetro

PageOutputCaching/VaryByParam.aspx
PageOutputCaching/VaryByParam.aspx?Id=1
PageOutputCaching/VaryByParam.aspx?Id=2

O parâmetro VaryByParam é obrigatório, se não quiser utilizá-lo deve-se informar o valor “none”. Para informar que todos os parâmetros passados devem ser considerados no caching utiliza-se o valor “*”. Porém devemos evitá-lo, pois para cada combinação de valores enviados para os parâmetros, uma página será armazenada em cache, com isso o espaço em memória reservado poderá rapidamente ficar escasso e outras informações mais importantes serem removidas.

2.5. Armazenamento em cache de acordo com controles da página

Assim como ocorre com a variação pela QueryString, é possível armazenar páginas em cache de acordo com os valores dos controles utilizados no Asp.Net. Para fazer isto basta configurar o parâmetro VaryByControl informando uma lista de ID’s dos controles que devem ser considerados no caching. Quando se utiliza o VaryByControl não é necessário informar o VaryByParam. O exemplo abaixo mostra como configurar o caching para que para cada valor selecionado e submetido do DropDowList “ddlTema” seja armazenada uma versão da página em cache.

Listagem 7: Cache variável por controle

<%--Configuração--%>
<%@ OutputCache Duration="1000000" VaryByControl="ddlTema" %>

2.6. Armazenamento em cache de acordo com parâmetro customizado

Também é possível, através de um valor customizado, definir como será o armazenamento em cache. Deve-se configurar o parâmetro VaryByCustom na página com um identificador, e no arquivo Global.asax sobrescrever o método GetVaryByCustomString, informando como deve ser realizado o processo. Abaixo um exemplo de como criar um caching da página de acordo com o método de requisição (Get ou Post).

Na página:

Listagem 8: Configurando a página para cache customizado

<%@ OutputCache Duration="1000000" VaryByParam="none" VaryByCustom="metodo"%>

Global.asx:

Listagem 9: Configurando o Global.asax para cache customizado

public override string GetVaryByCustomString(HttpContext context, string custom)
{
      if (custom == "metodo")
                return context.Request.RequestType;
      else
             return base.GetVaryByCustomString(context, custom);
}

2.7. Armazenamento em cache de acordo com cabeçalhos HTTP

Outra forma de mudar o comportamento do caching é configurando o parâmetro VaryByHeader, que recebe uma lista de propriedades enviadas com os cabeçalhos HTTP. Configurando esse parâmetro, para cada item de cabeçalho HTTP informado, irá ser armazenada uma versão da página de acordo com a combinação de seus valores. O exemplo abaixo mostra como configurar o caching para armazenar uma versão da página para cada valor do Accept-Language.

Listagem 10: Cache variável com cabeçalhos HTTP

<%@ OutputCache Duration="100000" VaryByParam="none" VaryByHeader="Accept-Language" %>

3. Dependência SQL

Um recurso bastante interessante oferecido pelo Asp.Net é a capacidade de inserir páginas no cache, configurando uma dependência direta com objetos do SQL Server. Você pode, por exemplo, colocar em cache uma página que acessa informação de uma tabela do banco de dados e estabelecer uma dependência com ela, assim o cache só será atualizado se ocorrerem alterações na tabela da base de dados.

Para estabelecer esta relação é necessário primeiramente configurar o SQL Server, para permitir o envio de notificações para o Asp.Net quando ocorrerem alterações na base dados. É preciso apenas executar o aplicativo aspnet_regsql.exe com os seguintes parâmetros.

Listagem 11: Configurando o SQL Server para enviar notificações ao ASP.NET

aspnet_regsql.exe -S <Servidor> -U <Usuário> -P <Senha> -ed -d <Base de dados> -et -t <Tabela>

Deve-se possuir privilégios de administrador para executar esta ação. Se ocorrer tudo certo, a seguinte mensagem irá ser exibida:

Listagem 12: Configuração do SQL Server bem sucedida

Enabling the table for SQL cache dependency.
..
Finished.

O próximo passo é configurar a dependência do caching no web.config. Para isso, na seção system.web, deve-se inserir a seguinte codificação:

Listagem 13: Alterando Configurando a dependência do caching no web.config

<caching>
      <sqlCacheDependency enabled="true" pollTime="1000">
          <databases>
               <add name="BaseDados" pollTime="1000" connectionStringName="cns" />
          </databases>
      </sqlCacheDependency>
</caching>

O exemplo acima cria uma dependência de caching para o banco de dados referenciado na connectionString. O próximo passo é configurar a dependência na página com o código seguinte:

Listagem 14: Configurando a dependência do caching na página

<%@ OutputCache Duration="100000" VaryByParam="none" SqlDependency="BaseDados:Produtos" %>

O código acima configura o caching da página com uma dependência SQL para tabela produtos, do banco de dados cadastrado no web.config. Com isso, após a primeira requisição, a página será armazenada em cache, que só será atualizado se ocorrer alguma alteração nos dados da tabela referenciada.

4. Caching profiles

Vimos como ativar o caching diretamente nas páginas, mas ao utilizar essa abordagem podemos trazer problemas para o projeto como a redundância de código e a dificuldade de manutenção. Pois se utilizarmos uma configuração padrão em cada página e quisermos, por exemplo, mudar o tempo de validade do caching de 10 para 30 segundos, teremos que realizar a alteração em todas as páginas que foram configuradas.

Para contornar esse problema é possível criar uma configuração padrão no web.config, e com isso, referenciá-la em nossas páginas criando apenas um ponto de manutenção. Abaixo um exemplo da configuração que deve ser inserida dentro da seção system.web.

Listagem 15: Configuração padrão do caching no web.config

<caching> 
     <outputCacheSettings>
          <outputCacheProfiles>
               <add name="cachingDefault" enabled="true" varyByParam="none" duration="10" />
          </outputCacheProfiles>
     </outputCacheSettings>
</caching>

O código acima cria um perfil de caching denominado “cachingDefault”, com duração de 10 segundos. Para utilizar esta configuração nas páginas, basta indicar no parâmetro CacheProfile, da diretiva OutputCache, o nome da configuração criada.

Listagem 16: Utilizando a configuração padrão do caching

<%@ OutputCache CacheProfile="cachingDefault" %>

5. Caching Parcial

Em alguns cenários, temos páginas que possuem seu conteúdo dinâmico variando, por exemplo, a cada usuário autenticado, mas que também possuem uma pequena porção de conteúdo estático, que seria ótimo candidato a ser armazenado em cache. Para esses casos é possível armazenar apenas a porção da página que não muda frequentemente, e quando ela for requisitada novamente, o conteúdo dinâmico será gerado e a parte estática retirada do cache, evitando o processamento desnecessário para obtenção dessas informações.

Para realizar este procedimento basta inserir o conteúdo estático em um User Control, e através da diretiva OutputCache, ativar o caching para o controle. O caching de controle possui um parâmetro extra, o Shared,que indica se o conteúdo em cache daquele controle poderá ser compartilhado por outras páginas. Se configurado como “true”, todas as páginas que o utilizam estarão compartilhando a mesma informação em cache, se não, irão armazenar uma versão para cada página.

O caching de controle é ativado da mesma forma que o de página, inserido através da diretiva OutputCache.

Listagem 17: Cache compartilhado

<%@ OutputCache Duration="10" VaryByParam="none" Shared="true" %>

Um ponto importante na utilização deste tipo de caching é que se o User Control possuir eventos e/ou propriedades, a página que o incorpora não deve acessá-los, pois no momento do caching ele não é gerado, apenas um bloco de HTML é inserido na página. Também não é possível utilizar os parâmetros Location, CacheProfile e NoStore. Assim como não se pode utilizar o parâmetro Shared em páginas .aspx.

6. Caching por substituição

Existem também cenários opostos ao anterior, onde a maior parte da página é de conteúdo estático e apenas uma pequena seção deve ser gerada dinamicamente. Para isso o Asp.Net possui o mecanismo de substituição de caching. Funciona de forma contrária a caching parcial, a informação enviada ao cliente é a que está armazenada em cache, apenas a pequena porção do conteúdo dinâmico será gerada e incorporada à página.

Há duas formas de se utilizar a substituição, a primeira é informar uma função de callback para o método WriteSubstitution do objeto Response, que recebe um delegate do tipo HttpResponseSubstitutionCallBack. Abaixo um exemplo de sua utilização.

Na página:

Listagem 18: Ativando cache por substituição na página

<%@ OutputCache Duration="10" VaryByParam="none" %>
...
<p>Dinâmico</p>
<asp:Label ID="lblTime" runat="server"></asp:Label>
<br /><br />

Inserindo conteúdo dinâmico
<p>Tempo dinâmico</p>
<%Response.WriteSubstitution(new HttpResponseSubstitutionCallback(GetTime));%>

No Code-Behind:

Listagem 19: Tratando o cache por substituição no code-behind

public static String GetTime(HttpContext ctx)
{
         return DateTime.Now.ToLongTimeString();
}

O exemplo acima exibe dois horários, o primeiro, que está em cache, só mudará após 10 segundos. O segundo, com o conteúdo dinâmico, a cada requisição será diferente.

A outra forma de realizar substituição é utilizando o controle Substitutition, configurando a propriedade MethodName com o nome do método que irá gerar o conteúdo dinâmico. Abaixo o exemplo anterior utilizado o controle.

Inserindo conteúdo dinâmico

<p>Tempo dinâmico</p>
<p><strong>Listagem 20</strong>: Utilizando o controle Substitution para gerar conteúdo dinâmico</p>
<asp:Substitution ID="subTime" runat="server" MethodName="GetTime" />

É importante que o método utilizado para gerar o conteúdo seja estático, pois quando a página estiver no cache nenhum objeto será instanciado.

7. Configurando o Caching via código

Para manipulações do caching via código, deve-se utilizar o objeto Response.Cache. Deve-se tomar cuidado para não confundi-lo com o Page.Cache, responsável pelo cache de objetos. O Response.Cache possui funcionalidades semelhantes a diretiva OutputCache e, para ativá-lo via código, deve-se utilizar os dois métodos abaixo.

  • Response.Cache.SetExpires: Funciona como o parâmetro Duration, configurando o número de segundos em que o cache será válido;
  • Response.Cache.SetCacheability: Este método trabalha como o Location, informando a localização do caching;

Listagem 21: Configurando o cache no code-behind

protected void Page_Load(object sender, EventArgs e)
{
        Response.Cache.SetExpires(DateTime.Now.AddSeconds(10));
        Response.Cache.SetCacheability(HttpCacheability.Server);
}

Com o objeto Response é possível estabelecer ainda relações de dependências para objetos CacheDependency, Arquivos ou outros objetos em cache. A seguir exemplos de como estabelecer a dependência para cada tipo.

Objetos CacheDependency: Para criar uma dependência com um objeto CacheDependency, utiliza-se o método Response.AddCacheDependency:

Listagem 22: Criando dependência no cahe no code-behind

protected void Page_Load(object sender, EventArgs e)
{
            System.Web.Caching.CacheDependency dependency = 
                                                              new System.Web.Caching.CacheDependency(
                                                              Server.MapPath("~/PageOutputCaching/TextFile.txt"));
           
                Response.AddCacheDependency(dependency);
 
             Response.Cache.SetCacheability(HttpCacheability.Server);
             Response.Cache.SetExpires(DateTime.Now.AddYears(100));
}

Arquivos: O objeto Response possui o método Response.AddCacheFileDependency e o Response.AddCacheFileDependencies, específico para estabelecer uma dependência com arquivos:

Listagem 23: Adicionando dependência de arquivo ao cache

protected void Page_Load(object sender, EventArgs e) 
{
      Response.AddFileDependency(Server.MapPath("~/PageOutputCaching/TextFile.txt"));
      Response.Cache.SetCacheability(HttpCacheability.Server);
      Response.Cache.SetExpires(DateTime.Now.AddYears(100));
}

Objetos em cache: Também é possível estabelecer uma relação de dependência com itens do Application Caching que estarão armazenados no objeto Page.Cache:

Listagem 24: Criando dependência de itens do cache


protected void Page_Load(object sender, EventArgs e) 
{
         Page.Cache.Insert("FileText", "devBrasil", null,                   
                                   System.Web.Caching.Cache.NoAbsoluteExpiration,
                                   new TimeSpan(0, 0, 10)); 
            Response.AddCacheItemDependency("FileText"); 
          Response.Cache.SetCacheability(HttpCacheability.Server);
          Response.Cache.SetExpires(DateTime.Now.AddYears(100));
}
 

8. Validação do caching

Em alguns cenários, pode ser interessante ignorar a versão da página em cache, forçando a geração de um novo conteúdo, ou melhor, ignorar temporariamente sem excluir o conteúdo que já está armazenado em cache, para propósitos de debug, por exemplo. O Asp.Net através do método Response.Cache.AddValidationCallback, registra uma função de call-back que permite, em tempo de execução, tomar decisões para validar ou não o processo de caching da página. O parâmetro passado deve ser um delegate do tipo HttpCacheValidateHandler, que entre seus parâmetros, recebe por referência um valor de enumeração do tipo HttpValidationStatus, que informa se o cache é válido, inválido ou deve ser ignorado.

Registrando o Callback:

Listagem 25: Registrando a função de callback


protected void Page_Load(object sender, EventArgs e)
{
      Response.Cache.AddValidationCallback(new HttpCacheValidateHandler(validateCache), null);
}

Método de validação:

Listagem 26: Implementando a função de callback

public static void validateCache(HttpContext ctx, object data, ref HttpValidationStatus status){
 
     string param = ctx.Request.QueryString["cache"]; 
 
     switch(param)   {
                case "invalido":
                                status = HttpValidationStatus.Invalid;
                               break;
                case "ignorar":
                                status = HttpValidationStatus.IgnoreThisRequest;
                                break;
                 default:
                                status = HttpValidationStatus.Valid;
                                break;
   }}

O exemplo acima registra o método validateCache como função de callback para validar o caching. O método utiliza o parâmetro “cache” passado via QueryString, e de acordo com seu valor configura o status da validação com um dos seguinte valores:

  • Invalid: Invalida o cache atual forçando a atualização;
  • IgnoreThisRequest: Ignora o cache atual forçando a geração da página mas não excluir o conteúdo que está no cache;
  • Valid: Informa que o conteúdo em cache é valido e pode ser utilizado.

Considerações finais

Assim como o Application Caching, o Page Output Caching é uma ótima ferramenta de aumento de desempenho, minimizando ao máximo as necessidades de processamento e acesso a base de dados, e ao mesmo tempo, mantendo-se consistente e atualizado, devido à utilização dos mecanismos de dependência. Porém, deve-se ter cuidado, pois se mal utilizado pode ter seus benefícios anulados, ou pior, a aplicação passar a exibir apenas informações desatualizadas e inconsistentes. Portanto, antes de sua utilização, deve haver um planejamento para verificar se o caching é a melhor ferramenta para o cenário.

Referências

  • NORTHRUP, Tony; SNELL, Mike.; MCTS Self-Paced Training Kit (Exam 70-515) - Web Applications Development with Microsoft .NET Framework 4.
  • MACDONALD, Matthew; FREEMAN, Adam; SZPUSZTA Mario. Pro Asp.Net 4 in C# 2010. 4. Ed.