Quase todos os aplicativos ASP.NET precisam controlar os dados da sessão de um usuário. ASP.NET fornece a classe HttpSessionState para armazenar valores de estado de sessão. Uma instância da classe HttpSessionState para cada solicitação HTTP pode ser acessada em todo o seu aplicativo usando a propriedade estática HttpContext.Current.Session. O acesso à mesma instância é simplificado em cada Page e UserControl usando a propriedade Session da Page ou UserControl.
A classe HttpSessionState fornece uma coleção de pares chave/valor, onde as chaves são do tipo String e os valores são do tipo Object. Isso significa que o Session é extremamente flexível e você pode armazenar praticamente qualquer tipo de dados no Session.
Mas (há sempre um mas) esta flexibilidade não vem sem um custo. O custo é a facilidade com que bugs podem ser introduzidos em sua aplicação. Muitos dos bugs que podem ser introduzidos não serão encontrados por testes unitários e provavelmente não por qualquer forma de teste estruturado. Esses bugs geralmente só aparecem quando o aplicativo é implantado no ambiente de produção. Quando eles surgem, muitas vezes é muito difícil, se não impossível, determinar como eles ocorreram e ser capaz de reproduzir o bug. Isso significa que eles são muito caros para consertar.
Este artigo apresenta uma estratégia para ajudar a prevenir esse tipo de bug. Ele usa um Design Pattern chamado Façade, na medida em que envolve a interface muito livre fornecida pela classe HttpSessionState (que pode atender aos requisitos de qualquer aplicativo) com uma interface bem projetada e controlada que é construída especificamente para um aplicativo específico. Se você não estiver familiarizado com Design Patterns ou Facade, uma rápida pesquisa na Internet por "padrão de design de fachada" fornecerá muitas informações básicas. No entanto, você não precisa entender os padrões de design para entender este artigo.
O código de exemplo mostrado neste artigo foi escrito em C#, mas os conceitos são aplicáveis a qualquer linguagem .NET.
Qual é o problema?
Nesta seção do artigo descreverei os problemas com acesso direto à classe HttpSessionState, sem fachada. Descreverei os tipos de bugs que podem ser introduzidos.
O exemplo a seguir mostra o código típico escrito para acessar variáveis de estado de sessão.
//Salva uma variável de sessão
Sessão["alguma string"] = anyOldObject;
//Lê uma variável de sessão
DateTime startDate = (DateTime)Session["DataInício"];
Os problemas surgem da interface flexível fornecida pelo HttpSessionState: as chaves são apenas strings e os valores não são fortemente digitados.
Usando literais de string como chaves
Se literais de string forem usados como chaves, o valor da string da chave não será verificado pelo compilador. É fácil criar novos valores de sessão com simples erros de digitação.
Sessão["recebida"] = 27;...
Sessão["recebida"] = 32;
No código acima, dois valores de sessão separados foram salvos.
A maioria dos bugs como esse serão identificados por testes unitários – mas nem sempre. Pode nem sempre ser aparente que o valor não mudou conforme o esperado.
Podemos evitar esse tipo de bug usando constantes:
private const string receiver = "received";...
Sessão[recebida] = 27;...
Sessão[recebida] = 32;
Sem verificação de tipo
Não há verificação de tipo dos valores armazenados nas variáveis de sessão. O compilador não pode verificar a exatidão do que está sendo armazenado.
Considere o seguinte código:
Session["maxValue"] = 27;...
int maxValue = (int)Sessão["maxValue"];
Em outros lugares, o código a seguir é usado para atualizar o valor.
Sessão["maxValue"] = 56,7;
Se o código para ler a variável de sessão "maxValue" na variável int maxValue for executado novamente, será lançada uma InvalidCastException.
A maioria dos bugs como esse serão identificados por testes unitários – mas nem sempre.
Reutilizando uma chave involuntariamente
Mesmo quando definimos constantes em cada página para as chaves de sessão, é possível usar involuntariamente a mesma chave nas páginas. Considere o seguinte exemplo:
Código em uma página:
private const string edit = "edit";...
Sessão[editar] = verdadeiro;
Código em uma segunda página, exibido após a primeira página:
private const string edit = "edit";...
if ((bool)Sessão[editar])
{
...
}
Código em uma terceira página não relacionada:
private const string edit = "edit";...
Sessão[editar] = falso;
Se a terceira página for exibida por algum motivo antes da segunda página ser exibida, o valor poderá não ser o esperado. O código provavelmente ainda será executado, mas os resultados estarão errados.
Normalmente esse bug NÃO será detectado nos testes. Somente quando um usuário faz alguma combinação específica de navegação na página (ou abre uma nova janela do navegador) é que o bug se manifesta.
Na pior das hipóteses, ninguém sabe que o bug se manifestou; podemos simplesmente acabar modificando os dados para um valor não intencional.
Reutilizando uma chave involuntariamente - novamente
No exemplo acima, o mesmo tipo de dados foi armazenado na variável de sessão. Como não há verificação de tipo do que é armazenado, também pode ocorrer o problema de tipos de dados incompatíveis.
Código em uma página:
Session["FollowUp"] = "true";
Código em uma segunda página:
Session["FollowUp"] = 1;
Código em uma terceira página:
Session["FollowUp"] = true;
Quando o bug se manifestar, será lançada uma InvalidCastException.
Normalmente esse bug NÃO será detectado nos testes. Somente quando um usuário faz alguma combinação específica de navegação na página (ou abre uma nova janela do navegador) é que o bug se manifesta.
O que podemos fazer?
A primeira solução rápida
A primeira e mais simples coisa que podemos fazer é garantir que nunca usaremos strings literais para chaves de sessão. Utilize sempre constantes e assim evite erros simples de digitação.
private const string limite = "limite";...
Sessão[limite] = 27;...
Sessão[limite] = 32;
Entretanto, quando as constantes são definidas localmente (por exemplo, no nível da página), ainda podemos reutilizar a mesma chave involuntariamente.
Uma solução rápida melhor
Em vez de definir constantes em cada página, agrupe todas as constantes de chave de sessão em um único local e forneça a documentação que aparecerá no Intellisense. A documentação deve indicar claramente para que serve a variável de sessão. Por exemplo, defina uma classe apenas para as chaves de sessão:
public static class SessionKeys
{
/// <resumo>
/// O máximo ...
/// </sumário>
public const string Limit = "limite";
}
...
Sessão[SessionKeys.Limit] = 27;
Quando você precisar de uma nova variável de sessão, se escolher um nome que já tenha sido usado, você saberá disso quando adicionar a constante à classe SessionKeys. Você pode ver como ela está sendo usada atualmente e determinar se deve usar uma chave diferente.
No entanto, ainda não estamos garantindo a consistência do tipo de dados.
Uma maneira muito melhor – usando uma fachada
Acesse HttpSessionState apenas de dentro de uma única classe estática em seu aplicativo - a fachada. Não deve haver acesso direto à propriedade Session de dentro do código em páginas ou controles, e nenhum acesso direto a HttpContext.Current.Session que não seja de dentro da fachada.
Todas as variáveis de sessão serão expostas como propriedades da classe de fachada.
Isto tem as mesmas vantagens de usar uma única classe para todas as chaves de sessão, além das seguintes vantagens:
Digitação forte do que é colocado nas variáveis de sessão.
Não há necessidade de conversão no código onde variáveis de sessão são usadas.
Todos os benefícios dos configuradores de propriedades para validar o que é colocado nas variáveis de sessão (mais do que apenas tipo).
Todos os benefícios dos getters de propriedades ao acessar variáveis de sessão. Por exemplo, inicializar uma variável na primeira vez que ela é acessada.
Um exemplo de classe de fachada de sessão
Aqui está um exemplo de classe para implementar a fachada Session para um aplicativo chamado MyApplication.
Colapso
/// <resumo>
/// MyApplicationSession fornece uma fachada para o objeto Session do ASP.NET.
/// Todo acesso às variáveis de sessão deve ser através desta classe.
/// </sumário>
classe estática pública MyApplicationSession
{
# região Constantes Privadas
//------------------------------------------------ ---------------------
private const string userAuthorisation = "UserAuthorisation";
string const privada teamManagementState = "TeamManagementState";
private const string startDate = "StartDate";
private const string endDate = "EndDate";
//------------------------------------------------ ---------------------
#endregion
#region Propriedades Públicas
//------------------------------------------------ ---------------------
/// <resumo>
/// O nome de usuário é o nome de domínio e nome de usuário do usuário atual.
/// </sumário>
string estática pública Nome de usuário
{
obter {retornar HttpContext.Current.User.Identity.Name; }
}
/// <resumo>
/// UserAuthorisation contém as informações de autorização para
/// o usuário atual.
/// </sumário>
autorização de usuário estática pública
{
pegar
{
Autorização de usuário userAuth
= (UserAuthorisation)HttpContext.Current.Session[userAuthorisation];
// Verifica se a UserAuthorisation expirou
se (
userAuth == nulo ||
(userAuth.Created.AddMinutes(
MyApplication.Settings.Caching.AuthorisationCache.CacheExpiryMinutes))
<DataHora.Agora
)
{
userAuth = UserAuthorisation.GetUserAuthorisation(Nome de usuário);
AutorAuthorização = userAuth;
}
retornar usuárioAuth;
}
conjunto privado
{
HttpContext.Current.Session[userAuthorisation] = valor;
}
}
/// <resumo>
/// TeamManagementState é usado para armazenar o estado atual do
/// Página TeamManagement.aspx.
/// </sumário>
público estático TeamManagementState TeamManagementState
{
pegar
{
retornar (TeamManagementState)HttpContext.Current.Session[teamManagementState];
}
definir
{
HttpContext.Current.Session[teamManagementState] = valor;
}
}
/// <resumo>
/// StartDate é a data mais antiga usada para filtrar registros.
/// </sumário>
público estático DateTime StartDate
{
pegar
{
if (HttpContext.Current.Session[startDate] == nulo)
retornar DateTime.MinValue;
outro
return (DateTime)HttpContext.Current.Session[startDate];
}
definir
{
HttpContext.Current.Session[startDate] = valor;
}
}
/// <resumo>
/// EndDate é a última data usada para filtrar registros.
/// </sumário>
público estático DateTime EndDate
{
pegar
{
if (HttpContext.Current.Session[endDate] == nulo)
retornar DateTime.MaxValue;
outro
return (DateTime)HttpContext.Current.Session[endDate];
}
definir
{
HttpContext.Current.Session[endDate] = valor;
}
}
//------------------------------------------------ ---------------------
# região final
}
A classe demonstra o uso de getters de propriedade que podem fornecer valores padrão se um valor não tiver sido armazenado explicitamente. Por exemplo, a propriedade StartDate fornece DateTime.MinValue como padrão.
O getter de propriedade para a propriedade UserAuthorisation fornece um cache simples da instância da classe UserAuthorisation, garantindo que a instância nas variáveis de sessão seja mantida atualizada. Esta propriedade também mostra o uso de um setter privado, de modo que o valor na variável de sessão só pode ser definido sob o controle da classe fachada.
A propriedade Username demonstra um valor que pode ter sido armazenado como uma variável de sessão, mas não é mais armazenado dessa forma.
O código a seguir mostra como uma variável de sessão pode ser acessada através da fachada. Observe que não há necessidade de fazer nenhuma conversão neste código.
//Salva uma variável de sessão
MyApplicationSession.StartDate = DateTime.Today.AddDays(-1);
//Lê uma variável de sessão
DateTime startDate = MyApplicationSession.StartDate;
Benefícios Adicionais
Um benefício adicional do padrão de design de fachada é que ele oculta a implementação interna do restante do aplicativo. Talvez no futuro você decida usar outro mecanismo para implementar o estado da sessão, diferente da classe interna do ASP.NET HttpSessionState. Você só precisa alterar a parte interna da fachada - não precisa alterar mais nada no restante do aplicativo.
Resumo
O uso de uma fachada para HttpSessionState fornece uma maneira muito mais robusta de acessar variáveis de sessão. Esta é uma técnica muito simples de implementar, mas com grandes benefícios.