Escolhendo o gerenciamento de estado no ASP.NET

Uma das principais necessidades em uma aplicação Web é a manutenção de estado. O protocolo HTTP determina que todas as aplicações que rodam sobre ele, não mantém nenhuma espécie de informação do lado do servidor, ou seja, ao requisitar qualquer recurso, uma vez que o conteúdo é enviado ao cliente solicitante, tudo o que foi gerado do lado do servidor para atende-lo, será descartado. Caso uma nova requisição ocorrer, mesmo que seja para o mesmo recurso, tudo será reconstruído.

Mas o que é o “estado”? Trata-se de dados que representam certas informações para o usuário ou para a aplicação, e que precisam ser armazenadas em algum local para conseguirmos restaurá-las no momento em que precisarmos delas. Depois que a requisição foi atendida pelo servidor, se ele precisar armazenar informações para aquele usuário, precisamos fazer uso de alguma das alternativas que são disponibilizadas pela tecnologia que estamos utilizando, e com isso, sermos capazes de manter esses dados mesmo que o usuário esteja totalmente “desconectado” da aplicação, para que quando ele voltar ao servidor, os dados continuarão disponíveis, refazendo o estado da requisição antes dela ser enviada ao navegador.

As alternativas que temos depende da tecnologia que estamos utilizando. No ambiente Microsoft, a tecnologia para desenvolvimento de aplicações Web é o ASP.NET. Ele, por sua vez, traz uma porção de opções para utilizarmos, e atualmente temos: Application, Caching, Controles Hidden, ViewState, Cookie, Session, Profile, QueryString e Context.Items. Para você escolher o que melhor se encaixa a sua necessidade, é necessário avaliar detalhes como segurança e performance. São essas duas categorias que mais influenciam na escolha de qual opção utilizar para manter as informações.

Para tentar auxiliar na escolha de cada um, criei um fluxograma que tenta exibir a melhor opção de acordo com a necessidade, balanceando entre escopo, performance e segurança. Abaixo temos a imagem que representa esse fluxograma, e na sequência, uma descrição mais detalhada de cada condicional que vemos nele.

  • Compartilhar: O compartilhamento consiste em ter uma informação que pode estar acessível entre vários usuários que acessam a mesma aplicação. Isso quer dizer que qualquer alteração que um usuário faça, já refletirá para todos aqueles que a acessam.
  • Dependência ou Expiração: Algumas vezes, a informação que é compartilhada entre os usuários poderá ter critérios de invalidação. Se você tem algo na memória e quer determinar uma forma de removê-la de lá, você pode optar por dependência ou expiração. A primeira delas consiste em ser reativo, ou seja, seremos notificado quando alguma mudança ocorrer na origem da informação, enquanto a segunda opção, devemos especificar um valor para determinar quando ela deverá ser removida.
  • Acessível entre requisições (escopo de usuário): A ideia desta condição é determina se os dados precisam ou não sobreviver entre a navegação das páginas da aplicação.
  • Dados Complexos ou Sigilosos (escopo de usuário): Para as informações que devem sobreviver entre todas as páginas da aplicação, podemos dividir em dados complexos ou sigilosos. Informações simples, como por exemplo, o idioma selecionado pelo usuário, podemos recorrer aos Cookies para armazená-las. Já aqueles dados que vão muito além do que simples strings, como é o caso de um carrinho de compras, podemos recorrer a recursos mais poderosos, como Session ou Profile.
  • Persistência: Utilizar Session ou Profile dependerá, principalmente, se você quer ou não ter a capacidade de persistir as informações. A principal diferença entre armazenar o carrinho de compras na Session ou não Profile, é que na segunda opção, as informações sobreviverão entre reinicializações do navegador e do servidor, ao contrário do que acontece com a Session [1], que é descartada ao fechar o navegador.
  • Transferência: A transferência consiste em levar dados de uma página para a outra, que geralmente parametrizam as tarefas que esta segunda página irá desempenhar.
  • Transferência no Servidor: Caso você já esteja executando algum código no servidor, então você pode optar pelo Context.Items para enviar os dados para uma outra página, sem a necessidade de voltar ao navegador, e a partir dali, ir para a página solicitada. Utilizar o Context.Items garante que as informações sejam passadas de forma transparente, e o usuário ou até mesmo os interceptadores de requisições HTTP não conseguirão capturar esses dados. Agora, se tivermos a necessidade de parametrizar publicamente a chamada de uma página, então a QueryString é a melhor saída, mas apenas lembre-se de que ela possue limitação de tamanho e você não deve passar dados sensíveis ali.
  • Manter dados na própria página: Para casos onde precisamos manter dados para uso excluso da página que estamos utilizando, podemos recorrer aos controles Hidden [2]. Esse tipo de controle é capaz de guardar informações simples, e o seu uso serve apenas para manter informações que não sejam sensíveis, estando tão vulneráveis quanto as QueryStrings.

[1] – A configuração padrão da Session no ASP.NET faz com que as informações sejam armazenadas InProc, ou seja, na memória do próprio servidor Web. Você pode alterar esse comportamento, e eleger um servidor SQL Server para persistir essas informações. Mas para essa situação, o Profile pode ser melhor, já que possui várias outras características interessantes, como a possibilidade de tipar as informações que serão armazenadas, migração de usuários anônimos, etc.

[2] – No caso do ASP.NET Web Forms, o ViewState também pode ser utilizado para manter as informações em nível de página, mas que irá recorrer ao uso de controles Hidden para o armazenamento delas. É importante dizer que o ViewState é armazenado nestes controles de forma codificada, ou seja, é possível extrair o que armazenamos ali. Seguindo a mesma linha de alguns itens anteriores, você não deve colocar informações sigilosas dentro dele.

Todos as opções para armazenamento do estado são basicamente dicionários, ou seja, utilizam a chave como sendo uma string. Já o valor dependerá do que está utilizando, por exemplo, no caso de Caching, Application, Session, Profile ou Context.Items, você poderá armazenar qualquer objeto, desde que ele esteja decorado com o atributo SerializableAttribute. As outras opções apenas trabalham com simples strings como valor.

Publicidade

Detectando a desconexão – Parte 2

Há algum tempo eu mostrei aqui como podemos proceder para detectar a desconexão do cliente de um serviço WCF. Aquela solução tem a finalidade de detectar a desconexão do cliente ao tentar enviar um callback para ele, onde podemos analisar o estado do canal de comunicação com o mesmo, e se estiver em estado falho ou se já foi fechado, podemos desprezar a notificação.

O problema daquela técnica é que o serviço somente saberá que o cliente não está mais ativo, quando tentarmos acessar o canal para se comunicar com ele. Se o cliente já encerrou a aplicação, de forma normal ou não (matando o processo), a referência dele ainda existirá dentro do nosso serviço.

Para sermos notificados de que o cliente foi encerrado, novamente, de forma normal ou não, podemos recorrer à implementação da interface IInputSessionShutdown. Para bindings que suportam sessões, essa interface pode ser aplicada ao runtime, e sermos notificados quando o cliente encerrar a conexão com o serviço. Essa interface fornece dois métodos: DoneReceiving e ChannelFaulted. O primeiro método é disparado quando o cliente não puder mais receber as notificações, pois ele encerrou o canal de comunicação com o serviço; já o método ChannelFaulted é disparado quando o canal de comunicação do cliente entra em estado falho, ou seja, algum problema ocorreu na aplicação consumidora do serviço, que não foi capaz de encerrar o proxy de forma explicíta. Abaixo temos uma implementação simples deste recurso:

public class SessionShutdownTrigger : IInputSessionShutdown
{
    public void ChannelFaulted(IDuplexContextChannel channel)
    {
        Console.WriteLine(“ChannelFaulted”);
    }

    public void DoneReceiving(IDuplexContextChannel channel)
    {
        Console.WriteLine(“DoneReceiving”);
    }
}

Como podemos notar, ambos métodos recebem como parâmetro um objeto que implementa a interface IDuplexContextChannel, que corresponde ao canal de comunicação do cliente. Com esta classe criada, precisamos acoaplá-la ao runtime do WCF, e para isso recorremos aos pontos de estensibilidade que o WCF fornece, mais precisamente, ao uso da interface IContractBehavior, que nos permite modificar, examinar ou estender aspectos pertinentes à um contrato. Ao implementar essa interface, entre os vários métodos que ela fornece, temos o método ApplyDispatchBehavior, que nos permite interceptar e aplicar algum recurso customizado ao dispatcher do serviço. Justamente por isso, como parâmetro recebemos uma instância da classe DispatchRuntime, que por sua vez, fornece uma propriedade chamada InputSessionShutdownHandlers, que nos permite adicionar instâncias de classes que implementam a interface IInputSessionShutdown. O código abaixo ilustra essa classe:

public class SessionShutdownTriggerBehaviorAttribute : Attribute, IContractBehavior
{
    public void ApplyDispatchBehavior(ContractDescription contractDescription,
        ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        dispatchRuntime.InputSessionShutdownHandlers.Add(new SessionShutdownTrigger());
    }

    //Outros Métodos
}

Essa classe por si só não funciona. Note que ela herda da classe Attribute, que me permite decorar a classe que representa o serviço, e com isso, ao rodá-lo, o WCF será capaz de perceber a presença deste atributo, e executar o método ApplyDispatchBehavior, incluindo a instância da classe SessionShutdownTrigger que criamos acima e, finalmente, quando a cliente encerrar o canal de comunicação, seremos notificados que isso ocorreu, e podemos executar algum código pertinentes aquele cliente, sem a necessidade de somente detectarmos isso quando precisarmos efetivamente comunicar com ele. Abaixo temos o contrato do serviço com esse atributo decorado:

[SessionShutdownTriggerBehavior]
public class Servico : IContrato
{
    public string Ping(string value)
    {
        return value;
    }
}