Estou trabalhando num projeto em que me deparei com um cenário diferente do que costumo encontrar quando o assunto é globalização (ou internacionalização para alguns), isso porque num projeto .net é mais do que de prache usar os arquivos de resource para solucionar este problema. Mas recentemente, ao iniciar um projeto aqui na empresa, fui surpreendido por um sistema diferente de globalização. Isso porque estava criando um sistema que deveria se comunicar com uma aplicação legada (desenvolvida em ASP), mas que tinha um recurso de tradução em que o cliente pode alterar e até mesmo incluir novos idiomas no sistema. O que é bem legal e não iríamos perder esta funcionalidade no novo sistema.

Neste novo cenário, fui obrigado a desenvolver uma “pattern” que aliviasse o banco do “stress” de consultas, pois imagine só… cada uma das expressões é requisitada uma a uma… então para mostrar uma simples tela de login por exemplo, seria necessário ir no banco várias vezes, pois precisaríamos dos textos de “Login”, “Senha”, “Entrar”, além de saudações e mensagens de erro.

Conhecendo o problema, agora bastava pensar na solução. Comecei desenvolvendo uma forma de pesquisar várias expressões ao mesmo tempo; fazendo com que o número de consultas SQL diminuisse. Fiz isso da seguinte forma:

– Separei os ids das expressões por contexto de página. Ou seja, separei todas expressõe que iria usar numa página ou grupo de páginas e adicionei num array identificado com o grupo de expressões (exemplo: “arrayLogin”);

var arrayLogin = new[] { 21, 28, 29, 1076, ......., 11467, 23, 1078, 1072, 952 };

– Criei um método que recebe esta identificação e o código do idioma e executa uma consulta e me retorna todas as expressões do array de uma só vez. Com isso, a primeira parte da minha estratégia de melhoria de performace estava funcionando. Agora eu precisava armazenar o resultado desta consulta e escolhi o “Cache” para fazer isso. Então comecei a montar minhas classes da seguinte maneira:

1 – Peguei cada linha do minha consulta e transformei numa estrutura como esta:

public class Expression {

    public int LanguageId { get; set; }
    public int ExpressionId { get; set; }

    public string Key { get; set; }
    public string Text { get; set; }

}

2 – Em seguida agrupei todas as linhas do meu datasource (agora em Expression´s) e os inseri num Dictionary<int, Expression>, onde o índice (inteiro) será o próprio ExpressionId. Desta forma, poderemos encontrar uma expressão específica usando Dictionary[ExpressionId].

3 – Agora eu precisava de uma classe que pudesse controlar quando pegar as expressões do banco ou do cache de acordo com a disponibilidade do cache (ou seja, da pesquisa já ter sido feita uma vez).

Para isso criei uma classe com alguns métodos estáticos chamada CacheManager.

public class CacheManager
{ 
    ///
    /// Arrays de com os códigos das expressões de globalizações
    ///

    static private int[] ArrayLogin = new[] { 514, 8307, 8679, 1083, .... , 887, 11472, 872, 10991 };
    static private int[] ArrayHome = new[] { 3333, 7052, 7054, 2778, .... , 832, 442, 8237 };
    static private int[] ArrayPagina = new[] { 202, 1182, 1182, .... , 782, 781, 813, 701, 783 };

    public static CacheDictionary CacheSection( int languageId, string sectionName )
    {

        string cacheKey = "Section_" + sectionName + "_Language_" + languageId;
        object cacheItem = HttpContext.Current.Cache.Get( cacheKey ) as Dictionary<int, Expression>;
        string fileName = HttpContext.Current.Server.MapPath( "~/Cache/" ) + cacheKey + ".txt";

        if ( ( cacheItem == null ) )
        {

            cacheItem = GetExpressions( languageId, GetArray(sectionName));                

            if ( !System.IO.File.Exists( fileName ) )
                System.IO.File.Create( fileName );

            var dep = new CacheDependency( fileName );

            HttpContext.Current.Cache.Insert( cacheKey, cacheItem, dep );

        }

        return new CacheDictionary( (Dictionary<int, Expression>)cacheItem, languageId );

    }

    ///
    /// Método que retorna o array de ids com base no nome da seção desejada
    ///
    private static int[] GetArray(string sectionName)
    {

        switch ( sectionName )
        {
            case "Login":
                return ArrayLogin;
            case "Home":
                return ArrayHome;
            case "Pagina":
                return ArrayPagina;            
            default:
                throw new ArgumentOutOfRangeException( "sectionName", sectionName, "Section not found." );
        }

    }

    ///
    /// Método que pesquisa as expressões no banco de dados com base nos ids presentes nos arrays
    /// 
    private static Dictionary<int, Expression> GetExpressions( int languageId, int[] codigos )
    {

        var list = new Dictionary<int, Expression>();
        var strSelect = @"SELECT * WHERE ExpressaoId IN ( @Codigos ) AND IdiomaId = @IdiomaId ";
        var strIds = string.Empty;

        for ( var i = 0; i < codigos.Length; i++ )
        {

            strIds += codigos[i].ToString();

            if ( i < ( codigos.Length - 1 ) )
                strSelect += " , ";

        }

        var command = new Command();

        command.AddWithValue( "IdiomaId", languageId );
        strSelect = strSelect.Replace("@Codigos", strIds);

        var expressionList = (DataTable)command.ExecuteQuery( strSelect, ReturnType.DataTable );

        foreach ( DataRow row in expressionList.Rows )
        {

            var expression = new Expression
                                 {
                                     ExpressionId = Convert.ToInt32( row["ExpressaoId"] ),
                                     LanguageId = Convert.ToInt32( row["IdiomaId"] ),
                                     Text = Convert.ToString( row["Texto"] )
                                 };

            list.Add( expression.ExpressionId, expression );

        }

        return list;

    }

}

Repare que o grande segredo da CacheManager são estas poucas linhas:

    ///
    /// Monta uma string padrão com o nome da seção e o código do idioma
    ///
    string cacheKey = "Section_" + sectionName + "_Language_" + languageId;

    ///
    /// Pega o objeto do cache do servidor com base na identificação (string montada acima)
    ///
    object cacheItem = HttpContext.Current.Cache.Get( cacheKey ) as Dictionary<int, Expression>;

    ///
    /// Verifica se existe um arquivo .txt no diretório da aplicação com o nome da identificação do cache
    ///
    string fileName = HttpContext.Current.Server.MapPath( "~/Cache/" ) + cacheKey + ".txt";

    ///
    /// Verifica se o objeto guardado no cache foi retornado com sucesso
    ///
    if ( ( cacheItem == null ) )

Caso o objeto exista, ele é retornado diretamente sem ir no banco de dados, precisando fazer apenas o “cast” do object.

 return new CacheDictionary( (Dictionary<int, Expression>)cacheItem, languageId );

Repare o que dicionário com as expressões é passado como parâmetro no construtor de outro objeto, o CacheDictionary. Este objeto foi criado para fazer o serviço de entregar com segurança as expressões requisitadas. Isso porque sabemos que se acessarmos Dictionay[9].Text, e o dicionário não tiver o índice de número 9 esta função irá retornar um erro e precisará ser tratado. Mas como este código estará presente em vários e inúmeros pontos de nosso aplicativo e caso ocorra um erro não queremos que o aplicativo pare de funcionar.

Para isso faremos o seguinte: quando uma expressão não for encontrada, ao invés de retornar o erro, iremos retornar um código que será mostrado na tela informando claramente que existe uma expressão faltando no banco de dados. Exemplo: por {908}, ou eng {908}. Que indicam que a expressão de português número 908 ou a expressão em inglês 908 está faltando no banco de dados.

public class CacheDictionary
{

    public Language Language { get; set; }
    public IDictionary<int, Expression> Dictionary { get; set; }

    public CacheDictionary(IDictionary<int, Expression> dictionary, int languageId)
    {
        if (dictionary != null) Dictionary = dictionary;
        Language = new Language(languageId);
    }

    public string GetExpressionText(int expressionId)
    {
         return Dictionary.ContainsKey(expressionId)
                       ? Dictionary[expressionId].Text
                       : Language.CultureInfo.ThreeLetterISOLanguageName + " : {" + expressionId + "}";
     }

}

Como podem ver, a principal vantagem desta estrutura é ter um tratamento de erros caso alguma expressão não exista, ao mesmo tempo que não precisamos ir no banco de dados consultar dados que já pesquisamos. Mas o mais importante é entender que o cache não fica armazenado nos arquivos de .txt que criamos no diretório fisico da aplicação, estes arquivos são apenas a referência de validade da consulta. Ou seja, se você já tiver um cache armazenado… e desejar atualizá-lo basta excluir os arquivos .txt referentes ao mesmo. E quando a aplicação verificar que o arquivo não existe… irá fazer uma nova consulta no banco, armazená-la no IIS e criar novamente o arquivo referente ao cache. Espero que estas dicas sejam úteis para alguém.

Desculpem pelo post grande, mas não vi como escrever mais resumido, pois do contrário não ficaria bem explicado. Abraços!