No post que vimos anteriormente falamos a respeito da infraestrutura necessária para criar uma solução flexível, visando ter uma independência de base de dados e que nos atendeu perfeitamente. O problema é que o acesso a qualquer fonte de dados (IO), seja banco de dados ou sistema de arquivos é muito custoso e, se fizermos isso durante toda e qualquer requisição para extrair as políticas de autorização, podemos ter problemas relacionados à performance da aplicação.
Quando habilitamos o uso das Roles do ASP.NET, então temos uma configuração que permite o armazenamento dos papéis do usuário em cookie e, com isso, apenas quando ele não for detectado é que o runtime do ASP.NET irá até a base para extrair os papéis do respectivo usuário. Essa solução é interessante, já que os papéis são exclusivos para cada um dos usuários. No caso das políticas de acesso às páginas da aplicação, isso refere-se à um recurso global e que não deve ser armazenado usuário por usuário.
Para evitar o round-trip excessivo ao provider (que no nosso caso é a base de dados), podemos criar uma variável global do tipo DBAuthorizationRuleCollection para armazenar todas as políticas de acesso da aplicação corrente. Com isso, apenas a primeira requisição fará o caminho completo, indo até o provider configurado, extraindo as informações e colocando-as no cache. A partir deste momento, as requisições subsequentes passam a utilizar este cache.
Uma vez que colocamos algum objeto no cache, passamos a ter um outro – pequeno – problema. Por quanto tempo isso vai “viver”? É importante lembrar que vamos criar uma área administrativa em que um usuário poderá customizar essas políticas e, qualquer mudança que ele fizer, devemos invalidar o cache existente. Talvez alguns querem alterar as políticas, mas não vê nenhum problema se essas mudanças não forem aplicadas imediatamente. Para atender a todos, criei um evento chamado DataChanged na classe DBAuthorizationProvider. Esse evento é disparado sempre quando uma nova política é criada ou excluída, e tem a finalidade de informar à aplicação que algo foi mudado, dando oportunidade para ela decidir se mantém ou não o cache.
Esse evento possui um argumento específico, chamado CachingRulesEventArgs. Essa classe tem uma propriedade booleana chamada ClearCache que, por padrão, é True e determina a limpeza do cache. Sendo assim, o momento ideal para assinar este evento é no evento Application_Start, acessível a partir do arquivo Global.asax. O exemplo abaixo ilustra como podemos determinar a limpeza ou não do cache, caso algum dado seja alterado durante a execução da aplicação:
DBAuthorization.Provider.DataChanged += (source, args) => args.ClearCache = true;
Como o padrão é True, somente faz sentido utilizar este código se quiser manter as políticas atuais em cache ou efetuar alguma tarefa customizada. A classe DBAuthorizationProvider possui um método virtual chamado OnDataChanged e, é dentro deste método que ela analisa se a aplicação se vinculou ou não ao evento, e caso verdadeiro o dispara. Finalmente, se a propriedade ClearCache retornar True, essa classe efetuará a limpeza do cache.
CacheManager é a classe estática responsável por gerenciar o cache e que é compartilhada por toda a aplicação. Além de manter a coleção das políticas da aplicação, também possui um objeto do tipo ReaderWriterLockSlim (namespace System.Threading) que gerencia o acesso concorrente ao cache. Esse tipo de objeto garante múltiplas threads para leitura ou o acesso exclusivo para escrita.
É importante dizer que, por mais custoso que seja, o desenvolvedor pode optar por querer sempre consultar a base de dados e, justamente por isso, que criei uma propriedade booleana chamada StoreRulesInCache, que indica ao runtime se os dados devem ou não serem armazenados no cache, sendo o padrão True.