Páginas

sábado, 31 de março de 2012

"Novidades" no Visual Studio 11 Beta - Microsoft Fakes

Olá, pessoal!

Hoje vou falar de uma das "novidades" que vieram na versão beta do Visual Studio 11: Microsoft Fakes. O Fakes é um framework de geração de objetos dublês para testes, ou seja, ele serve para criar classes que possam substituir outras para facilitar a execução de testes de caixa branca. Testes de unidade devem verificar apenas o comportamento da classe que estão testando, e não de várias classes. Por exemplo, se para testar uma classe de negócio precisamos da sua classe de dados, para retornar algumas informações, então isso não é um teste de unidade...

É mais fácil mostrar a utilidade do Fakes com um exemplo. Antes de tudo, você deve baixar a versão beta do Visual Studio 11 neste link http://www.microsoft.com/visualstudio/11/en-us.

Como exemplo, eu criei um projeto do tipo "Unit Test Project", para C#. Vou colocar o próprio código da classe de negócios que será testada neste projeto. Numa solução real, isso não poderia ser feito, mas como é apenas uma demonstração, eu tomei a liberdade de cometer essa atrocidade.

No projeto, eu criei uma classe chamada ClasseDeNegocio. Essa classe será responsável por excluir um arquivo do disco caso ele tenha sido criado há mais de 2 meses. Abaixo está o código dessa classe, com o método ExcluirArquivoAntigo.

using System;
using System.IO;

namespace ExemploMicrosoftFakes
{
    public class ClasseDeNegocio
    {
        public void ExcluriArquivoAntigo(string arquivo)
        {
            // verifica se o arquivo existe
            if (File.Exists(arquivo))
            {
                // verifica se é mais velho que 2 meses atrás (regra do sistema)
                if (File.GetCreationTime(arquivo) < DateTime.Now.AddMonths(-2))
                {
                    // sendo assim, exclui
                    File.Delete(arquivo);
                }
            }
        }
    }
}

Em resumo, este método verifica se um determinado arquivo existe e se ele é mais velho que dois meses. Se for, então ele é excluído. Veja um detalhe: ele utiliza vários métodos estáticos de classes do .NET Framework, como File e DateTime. Como poderíamos isolar este tipo de chamada, colocando um objeto dublê de testes? A resposta está no "novo" framework de testes da Microsoft, o Fakes. Veja os três testes de unidade abaixo, que testam as seguintes situações:
  1. Se o arquivo não existe, então ele não deve ser excluído.
  2. Se o arquivo existe e é mais velho que dois meses, então deve ser excluído.
  3. Se o arquivo existe mas não é mais velho que dois meses, então deve ser mantido.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.QualityTools.Testing.Fakes; // para ShimsContext
using System.IO.Fakes;  // para o ShimFile
using System.Fakes; // para o ShimDateTime


namespace ExemploMicrosoftFakes
{
    [TestClass]
    public class TesteDeUnidade
    {
        [TestMethod]
        public void ClasseDeNegocio_ConsultarConteudoArquivo_NaoDeveExcluirArquivoInexistente()
        {
            using (ShimsContext.Create())
            {
                // Arrange
                bool excluido = false;
                ShimFile.ExistsString = (s) => false;
                ShimFile.DeleteString = (s) => excluido = true;
                var alvo = new ClasseDeNegocio();
                // Act
                alvo.ExcluriArquivoAntigo("xxx");
                // Assert
                Assert.AreEqual(false, excluido);
            }
        }

        [TestMethod]
        public void ClasseDeNegocio_ConsultarConteudoArquivo_DeveExcluirArquivoMaisAntigoQue2MesesDaDataAtual()
        {
            using (ShimsContext.Create())
            {
                // Arrange
                ShimDateTime.NowGet = () => DateTime.Parse("2011-04-01");
                bool excluido = false;
                ShimFile.ExistsString = (s) => true;
                ShimFile.GetCreationTimeString = (s) => DateTime.Parse("2011-01-01");
                ShimFile.DeleteString = (s) => excluido = true;
                var alvo = new ClasseDeNegocio();
                // Act
                alvo.ExcluriArquivoAntigo("xxx");
                // Assert
                Assert.AreEqual(true, excluido);
            }
        }

        [TestMethod]
        public void ClasseDeNegocio_ConsultarConteudoArquivo_NaoDeveExcluirComAte2MesesDaDataAtual()
        {
            using (ShimsContext.Create())
            {
                // Arrange
                ShimDateTime.NowGet = () => DateTime.Parse("2011-04-01");
                bool excluido = false;
                ShimFile.ExistsString = (s) => true;
                ShimFile.GetCreationTimeString = (s) => DateTime.Parse("2011-03-01");
                ShimFile.DeleteString = (s) => excluido = true;
                var alvo = new ClasseDeNegocio();
                // Act
                alvo.ExcluriArquivoAntigo("xxx");
                // Assert
                Assert.AreEqual(false, excluido);
            }
        }
    }
}

Vejam as classes ShimDateTime e ShimFile, elas são o segredo para fazer os testes funcionarem. Vamos olhar a linha ShimFile.ExistsString = (s) => false;. O que estamos fazendo aqui é substituir (isso mesmo, substituir) o método File.Exists para que ele tenha outro comportamento. No caso, o que queremos  é que ele simplesmente retorne false para que possamos testar o cenário onde o arquivo não existe, por isso estamos passando para ShimFile.ExistsString um delegate que retorne false.

Ainda comentando sobre o primeiro exemplo, como podemos garantir que nossa classe de negócio realmente funciona e quando o arquivo não existe ela realmente não faz a exclusão? Para isso, podemos interceptar também o método File.Delete, fazendo com que caso ele seja chamado, nós ajustamos o valor da varíável excluido para true. Ao final do teste, quando fazemos o "Assert", se excluido continuar com o valor false, quer dizer que o método File.Delete não foi chamado, confirmando que o cenário está ok. Se File.Delete foi chamado, então há algo de errado com nossa classe de negócios...

Ok, você entendeu que as classes Shim servem para interceptar as chamadas de métodos reais, mas de onde elas vêm? Bom, para que essas classes estejam disponíveis, é necessário criá-las. Mas calma, quem vai fazer esse trabalho é o Visual Studio. Veja como é simples: basta clicar com o botão direito sobre o componente do qual queremos interceptar os métodos de suas classes, e escolher a opção "Add Fakes Assembly". No nosso exemplo, nós adicionamos as classes Shim para todos as classes do namespace System do próprio .NET Framework (File e DateTime vão junto neste pacote...).

Após esse passo, veja que foi criada uma pasta chamada Fakes, contendo os arquivos mscorlib.fakes e System.fakes. São esses arquivos que são usados para a criação dos assemblies fakes. Note que com isso, duas novas referências aparecem no projeto: mscorlib.4.0.0.0.Fakes.dll e System.4.0.0.0.Fakes.dll.

Bom, agora só falta executar os métodos de testes para você acreditar no que eu estou dizendo. Para isso, basta ir na janela Unit Test Explorer do Visual Studio 11 beta, e clicar em "Run All". Outra opção para fazer isso é clicar com o botão direito sobre a classe de testes e clicar em "Run Unit Tests".


Com isso, vimos como é fácil criar testes de unidade no Visual Studio, isolando as nossas dependências e permitindo que os testes realmente validem apenas um pedaço do código. Mas ainda sobram algumas perguntas a serem respondidas...

Eu já não vi isso antes?


Sim, interceptação e substituição de métodos não são novidades no Visual Studio. O framework Moles (http://research.microsoft.com/en-us/projects/moles/ e http://galorebr.blogspot.com.br/2011/07/artigo-de-moles-net-magazine-87.html) já fazia isso! É praticamente a mesma coisa! Foi por isso que no título desse post eu coloquei a palavra novidades entre aspas...

Mas uma diferença que notei está relacionado a forma com que a interceptação é feita. Enquanto no Moles era necessário um processo exclusivo para rodar o código dos testes de unidade (Microsoft.Moles.VsHost.x86.exe), e dessa forma poder usar o profiler para interceptar as chamadas no .NET, no Fakes é utilizado o profiler do IntelliTrace para isso. Na minha opinião isso é uma boa melhoria, pois ficamos com apenas um profiler ao invés de dois. Além disso, até onde testei, a execução do Fakes não tem alguns problemas que existiam com o Moles: às vezes davam alguns erros na execução dos testes, e o processo Microsoft.Moles.VsHost.x86.exe não era finalizado, gerando problemas na hora de executar os testes novamente.

Bom, na própria página do Moles há a mensagem de que ele será descontinuado para a entrada do Microsoft Fakes. Mas aparentemente não há muitas dificuldades em converter o código que usava Moles para usar o Fakes. O seguinte link mostra com mais detalhes como utilizar o Shim do Fakes para outras situações, além dos Stubs (que também já estavam disponíveis no Moles): http://msdn.microsoft.com/en-us/library/hh549175(v=vs.110).aspx.

O código de exemplo que fiz para este post pode ser baixado em https://github.com/fabiogouw/Exemplos

[]'s

terça-feira, 27 de março de 2012

Annoying Manager

Olá, pessoal.

Há algum tempo eu vinha querendo criar alguma coisa e disponibilizar como código aberto.

Hoje finalmente fiz isso, e o fiz com uma idéia de aplicativo que eu tive lá por 2004, mas nunca cheguei a concluir: uma ferramenta para ajudar a controlar as atividades que vou fazendo durante o dia, algo que me forçasse a manter um histórico de tudo o que vou fazendo. Na época esse aplicativo iria se chamar "C.H.A.T.O", que era um acrônimo de "Controle de Horários, Atividades e Tarefas Operacionais" (sim, forcei para que pudesse ser um acrônimo com sentido, rs).

Hoje essa ferramenta se chama Annoying Manager (Gerente "Pentelho"), e está disponível no Codeplex, site de projetos abertos mantido pela Microsoft. O endereço é http://codeplex.com/annoyingmanager/.

A idéia é a seguinte: um aplicativo que executa em segundo plano e de tempos em tempos aparece para questionar o que você está fazendo no exato momento. Você dá uma descrição da atividade para ele, e então ele "dorme" por mais alguns minutos. Ao final, você tem uma listagem de tudo o que fez.

O programa está em versão beta. Utilizando-o no dia à dia, já identifiquei alguns problemas e melhorias que podem ser feitas, e pretendo implementá-las logo. Uma das coisas novas que tenho em mente é migrar a interface para WPF, e outra é buscar as atividades de um servidor TFS.

Convido quem quiser a participar desse projeto, afinal ele é open-source. O código-fonte está disponível no mesmo endereço que passei, bem como o download do executável e uma lista de discussões.

[]'s

sexta-feira, 23 de março de 2012

Relato de Caso: Uso de cache em aplicação ASP.NET

Olá, pessoal.

Hoje vou falar sobre cache, mas quero fazer algo diferente dos muitos materiais que se encontra na Internet. Não numa visão de codificação prática, mas sim conceitualmente, explicando o resultado que tive ao implementá-lo em uma aplicação ASP.NET que possuía sérios problemas de desempenho.

Primeiramente, qual é o conceito de cache? A idéia do cache é ter uma cópia de um conjunto de dados para acesso rápido. Dessa forma não oneramos o repositório original, cujo acesso é mais lento que o cache, ao fazer buscas.

A seguinte imagem mostra como um algoritmo de cache comumente é feito. Alguém faz uma requisição ao cache, buscando determinada informação. Caso essa informação não exista no cache, ele vai ao repositório original para carregá-la e o insere no cache. A partir disso, qualquer busca por esta informação no cache vai trazer o dado armazenado, ao invés de efetuar a pesquisa no repositório “lento”.



A vantagem do cache está em melhorar o desempenho de pesquisas. O principal desafio dessa abordagem é controlar a validade do cache: pode ser que logo após a informação ser armazenada no cache, ela ser atualizada no repositório original. Nesse caso, que for acessar o cache irá receber uma versão antiga. Isso pode ser problema para algumas situações, e existem diversas formas de contornar este problema. Uma delas que minimiza esse efeito indesejado é estabelecer um tempo máximo de vida do cache, ou seja, depois de determinado tempo, um objeto armazenado no cache é descartado e uma nova consulta no repositório original quando o objeto for novamente requisitado. Essa é a abordagem que utilizei com o cache do ASP.NET, e que serviu bem para a situação que trabalhei.

O cenário que tínhamos era o seguinte. Uma aplicação era disponibilizada na Internet para acesso de vários estabelecimentos comerciais e seus procuradores para receber avisos e notificações. Pelo grande volume atual de acessos, esse sistema não estava mais escalando e ficava indisponível constantemente. Quando cheguei, a primeira coisa que fiz foi tirar um dump de memória, e com isso constatei que havia muitos objetos do tipo Estabelecimento carregados no processo do IIS. Depois de uma análise de código, entendemos que essa quantidade excessiva de objetos era porque se fazia muitas consultas ao banco de dados buscando este tipo de objeto. As informações armazenadas neles, por exemplo CNPJ, razão social e data de fundação, não eram dados que recebiam muitas atualizações (alias, é bem difícil que recebam), e por isso optamos por colocar uma rotina de cache para esses objetos. Utilizamos o CNPJ do estabelecimento como índice de busca. Outro detalhe importante é que essas informações de estabelecimentos eram para somente leitura. Com isso, problemas de concorrência no uso desses objetos praticamente não existem.

Como o sistema já estava dividido numa arquitetura que, apesar de anêmica, possuía a divisão clara de componentes na qual as classes de acesso a dados já estavam bem separadas, decidimos colocar o cache neste ponto. Usamos sim o cache do ASP.NET dentro da camada de acesso a dados, no entanto isolamos essa chamada à infra-estrutura do ASP.NET de forma que se esse sistema mude de plataforma, possamos acoplar outra solução de cache (como o AppFabric ou NCache, por exemplo) de forma simples.

Feito isso, testamos e implantamos em produção. Felizmente deu tudo certo e a aplicação voltou a atender os usuários!

Fizemos algumas medições para ver como o uso do cache melhorou o desempenho. Podemos ver isso no gráfico abaixo.


Este gráfico é o Performance Monitor do Windows. Nós pedimos para ele acompanhar alguns contadores de desempenho referentes ao cache do ASP.NET: Cache API Entries, Cache API Hits e Cache API Misses (os três primeiros contadores que aparecem). Para poder explicar melhor o que cada um deles representa, vamos recorrer ao desenho que comentamos agora há pouco. Veja a nova versão abaixo.



  • Cache API Misses representa a quantidade de vezes que buscamos um determinado objeto no cache e ele não se encontra. Neste caso, temos que ir no repositório "lento".
  • Cache API Entries representa a quantidade de objetos que foram recuperados do repositório original e que estão atualmente armazenados no cache.
  • Cache API Hits representa quantas vezes fomos até o cache e achamos a informação que queríamos já pronta para nosso uso. É neste momento que vemos o ganho de desempenho.
Com essas informações na cabeça, vamos analisar o gráfico. Vejam que a linha azul (Cache API Hits) é a que mais cresce, enquanto as outras ficam relativamente constantes. Isso indica que o cache está sendo bem utilizado, pois temos uma mesma informação (linha vermelha, o Cache API Entries) que está sendo consumida inúmeras vezes.

Alguns podem perguntar da relação entre a linha vermelha e a linha rosa (Cache API Misses), pois na teoria toda vez que não encontramos uma informação no cache (miss), nós a carregados do repositório. Neste caso, os valores de ambos os contadores deveriam ser iguais (um para um). O que acontece nesse caso é que objetos do cache que já tiveram seu tempo de vida expirado são removidos, fazendo com que seja necessária uma nova busca para uma informação que já foi pesquisada há tempos.

O que eu gostaria de falar para vocês é que nesta situação que tivemos, o uso de cache foi imprescindível para um bom desempenho do sistema. Tenham sempre em mente que é muito provável que seja vantajoso utilizar uma estratégia de cache quando temos a mesma informação sendo consumida várias vezes. É claro que cada caso requer uma análise dos problemas colaterais que podemos ter, como maior uso de memória e consumo de dados desatualizados, mas eu acredito que o custo, ao menos da análise, valha a pena.

Aqui seguem alguns links com mais informações sobre cache:
Até a próxima!

segunda-feira, 19 de março de 2012

Criando e testando objetos com construtores privados

Imaginem a seguinte situação. Temos uma classe qualquer. Essa classe possui uma dependência. Além disso, vamos supor que a criação desse objeto seja uma coisa complexa, ou seja, não podemos simplesmente deixar que essa classe seja instanciada por um new.

// MeuObjeto.cs
    public class MeuObjeto
    {
        public IMinhaDependencia Dependencia { get; set; }

        internal MeuObjeto()
        {
            // internal para garantir que apenas
            // a factory possa criar este objeto
        }

        public int RetornarCalculo(int a)
        {
            return Dependencia.Calcular() + a;
        }
    }

    // IMinhaDependencia.cs
    public interface IMinhaDependencia
    {
        int Calcular();
    }

Temos então uma classe factory para criar uma instância desse objeto. Problema resolvido, correto? Se eu precisar de uma instância dessa classe, simplesmente chamo sua factory.

// MeuObjetoFactory.cs
    public class MeuObjetoFactory
    {
        public MeuObjeto CriarObjeto()
        {
            return new MeuObjeto()
            {
                Dependencia = new MinhaDependencia()
            };
        }
    }

    // MinhaDependencia .cs
    // faz de conta que é um objeto muito custoso para criar
    public class MinhaDependencia : IMinhaDependencia
    {
        public int Calcular()
        {
            return 1;
        }
    }

Do ponto de vista funcional, funciona que é uma beleza. Mas como podemos testar unitariamente a classe MeuObjeto? Neste caso, teríamos duas opções:

  1. Usamos a factory no nosso teste de unidade para criar o objeto. O problema nesse caso é que nosso teste de unidade passaria a testar a classe MeuObjeto, a factory MeuObjetoFactory além de obrigatoriamente criar uma instância da dependência MinhaDependencia, o que implica que a testaríamos a sua criação também. Isso deixou de ser um teste de unidade.
  2. Alteramos o escopo de MeuObjeto e deixamos seu construtor público. Com isso, podemos instanciá-lo diretamente, sem usar nada do factory nem criar sua dependência, e passar um objeto dublê de teste (mock) para simular o comportamento da dependência. Só que com isso, qualquer desavisado poderia instanciar diretamente MeuObjeto, e era exatamente isso que queríamos evitar quando fizemos seu construtor com escopo internal (visível apenas dentro do assembly que ele reside) e criamos a factory.
E agora, qual opção seguir?

Bom, temos uma terceira opção, pegando o que há de bom em cada cenário acima, sem suas desvantagens!

Vamos usar uma classe chamada PrivateObject, disponível no namespace Microsoft.VisualStudio.TestTools.UnitTesting. Com ela, podemos criar e chamar métodos de qualquer classe, mesmo que esses métodos sejam privados. Olhem só a classe de testes abaixo.

// UnitTest1.cs
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        [Ignore]
        public void TestMethod1()
        {
            //var obj = new MeuObjeto(); -- isso dá erro de compilação
        }

        [TestMethod]
        public void TestMethod2()
        {
            // Arrange
            var mock = new Mock<IMinhaDependencia>();
            mock.Setup(m => m.Calcular()).Returns(1);
            var po = new PrivateObject(typeof(MeuObjeto));
            var obj = (MeuObjeto)po.Target;
            obj.Dependencia = mock.Object;
            // Act
            var resultado = obj.RetornarCalculo(2);
            // Assert
            Assert.AreEqual(3, resultado);
        }
    }

O método TestMethod1 é só para mostrar que não funciona instanciar direto um objeto com construtor não visível (privado ou como no nosso caso, internal). Vamos olhar o método TestMethod2, que é mais interessante para nós. A primeira coisa que ele está fazendo é criar um objeto dublê de teste usando o framework Moq. Não é escopo deste post explicar mocks ou o Moq, então aqui está um link para mais informações http://code.google.com/p/moq/wiki/QuickStart. Mas explicando de forma bem rápida, o que estamos fazendo é criar um objeto que possua os mesmos métodos da interface IMinhaDependencia, e ajustando o comportamento que ele terá quando formos chamar o seu método Calcular (no nosso exemplo, estamos cravando que seu retorno será o valor 1). Com isso, "enganamos" a instância de MeuObjeto, fazendo com que ele acredite que esteja trabalhando com uma implementação concreta.

Bom, voltando ao assunto original, chegamos agora na parte onde efetivamente criamos o objeto de construtor não-visível. Simplesmente criamos uma instância de PrivateObject passando o tipo que queremos que ele crie. De forma "mágica", ele nos cria esse objeto (internamente ele deve usar .NET Reflection para criar a instância, mas como eu não tenho certeza disso, considero que é por mágica). Assim, conseguimos pegar a sua referência acessando a propriedade Target de PrivateObject. Com isso, podemos continuar nosso teste de unidade, passando o mock que foi gerado e testando a sua funcionalidade dentro do teste de caixa-branca.

Por hoje é só. Em breve eu disponibilizo os fontes desse exemplo em algum repositório público (ainda estou decidindo se eu uso o BitBucket ou o GitHub).

[]'s

ATUALIZAÇÃO: Disponibilizei o exemplo em https://github.com/fabiogouw/Exemplos. Decidi deixar minha conta no Github para coisas públicas, e o Bitbucket para coisas pessoais.

sexta-feira, 9 de março de 2012

ORA-01843: not a valid month

Estou aqui de novo para comentar problemas relacionados ao desenvolvimento com Oracle, e como eles foram resolvidos.

Na equipe que trabalho entrou uma pessoa nova, e precisamos instalar o cliente do Oracle para que ele pudesse trabalhar. Após feita a instalação do Oracle Instant Client, quando ele começou a executar o sistema na sua máquina, o seguinte erro apareceu: ORA-01843: not a valid month. Detalhe que quando a mesma rotina era executada na minha máquina, eu não tinha esse problema.

Após algumas pesquisas, identificamos a causa. No banco de dados que estamos utilizando, a configuração de formato de data está parametrizado para dia/mês/ano:


select * from nls_database_parameters;

NLS_LANGUAGE                          AMERICAN
NLS_TERRITORY                          AMERICA
NLS_CURRENCY                          $
NLS_ISO_CURRENCY                  AMERICA
NLS_NUMERIC_CHARACTERS  .,
NLS_CHARACTERSET                  WE8ISO8859P1
NLS_CALENDAR                         GREGORIAN
NLS_DATE_FORMAT                 DD-MON-RR
NLS_DATE_LANGUAGE                  AMERICAN
NLS_SORT                                         BINARY
NLS_TIME_FORMAT                         HH.MI.SSXFF AM
NLS_TIMESTAMP_FORMAT         DD-MON-RR HH.MI.SSXFF AM
NLS_TIME_TZ_FORMAT                 HH.MI.SSXFF AM TZR
NLS_TIMESTAMP_TZ_FORMAT DD-MON-RR HH.MI.SSXFF AM TZR
NLS_DUAL_CURRENCY                $
NLS_COMP                                        BINARY
NLS_LENGTH_SEMANTICS        BYTE
NLS_NCHAR_CONV_EXCP        FALSE
NLS_NCHAR_CHARACTERSET    AL16UTF16
NLS_RDBMS_VERSION               11.2.0.2.0


No entanto, na máquina do desenvolvedor não havia configuração de como o cliente do Oracle trataria as informações. Desse modo, ele estava considerando o retorno de formato de data como no padrão mês/dia/ano. Quando a rotina era executada, ela retornava uma data, digamos 16/02/11. No formado brasileiro, teríamos o 16 como dia, mas lendo isso no formado padrão, 16 seria o mês, o que não existe. Por isso ocorria o erro de not a valid month.

A correção foi incluir uma parametrização no registro do Windows informando que era para ser considerado o formato brasileiro. Ou seja, incluir a propriedade NLS_LANG com valor BRAZILIAN PORTUGUESE_BRAZIL.WE8MSWIN1252 dentro da chave HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE\KEY_OraClient11g_home1 (este caminho pode variar, dependendo de como foi feita a instalação do cliente).


Mas muito importante, tivemos que reiniciar a máquina para essa parametrização surtir efeito. Depois disso, tudo funcionou ok.

Obs. Eu não sei se essa é a forma correta de corrigir o problema, mas foi o que funcionou. Se alguém conhecer um jeito mais elegante, por favor, comente aqui.

ATUALIZAÇÃO: 23/05/2013

Com uma máquina nova, novamente tive que recorrer à Internet para resolver este problema. Infelizmente, não foi possível utilizar esta mesma solução, pois na estação que estou usando o Oracle não está instalado com o instalador, mas simplesmente copiando os binários e ajustando o caminho nas variáveis de ambiente do Windows (Path). Por isso, não estava disponível a chave no registry que comentei acima.

A solução? Criar uma nova variável de ambiente, chamada NLS_LANG, onde coloquei novamente o valor BRAZILIAN PORTUGUESE_BRAZIL.WE8MSWIN1252. Reiniciei e tudo funcionou.

[]'s

quarta-feira, 7 de março de 2012

TransactionScope e Invalid operation. The connection is closed no Oracle

Tudo na vida acontece por uma razão, e quando a gente não entende a razão considera que é magia.

Digo isso por um problema que estava tendo em uma aplicação ASP.NET que usa banco de dados Oracle. Em determinada funcionalidade, um processamento com gravação, ocorria um problema que impedia a gravação dos registros. O erro era:

System.InvalidOperationException: Invalid operation. The connection is closed.
   at System.Data.OracleClient.OracleConnection.GetOpenInternalConnection()
   at System.Data.OracleClient.OracleConnection.get_ErrorHandle()
   at System.Data.OracleClient.OracleDataReader.ReadInternal()
   at System.Data.OracleClient.OracleDataReader.Read()
   at Microsoft.Practices.EnterpriseLibrary.Data.Oracle.OracleDataReaderWrapper.Read()

Simplesmente eu não entendia a causa desse erro ( neste momento chamado de "magia"), pois o código que rodava essa consulta (sim, consulta, pois você pode ver que na stacktrace estamos usando o DataReader do OracleClient) funcionava de forma isolada sem problemas. Essa conclusão eu cheguei após gerar um windows service e colocar para ficar rodando este método a cada 30 segundos, já que minha primeira desconfiança era algum problema esporádico de comunicação com o servidor Oracle. Bom, esse windows service ficou rodando por horas, sem apontar nenhum problema.

No final do dia, uma coisa me chamou a atenção: processamentos pequenos funcionavam sem problema, os que gravavam muitos dados é que geravam o erro. E mais, todo o código que era executado estava envolvido por um TransactionScope... Na altura do campeonato, resolvi testar tirando a transação, ou seja, passando no seu construtor a opção Suppress (http://msdn.microsoft.com/en-us/library/ms172152(v=vs.90).aspx):

using(var trans = new TransactionScope(TransactionScopeOption.Suppress)) 
{
    // ...
} 

Dito e feito. Após essa alteração, o código funcionou.

Mas não deixei a rotina assim. Para não ficar com ela de forma que pudesse dar alguma inconsistência na gravação do banco, eu mantive a transação ativa só que coloquei um tempo de timeout maior (http://msdn.microsoft.com/en-us/library/9wykw3s2.aspx). Também funcionou e resolveu o problema.

Em resumo, o problema do Invalid Operation dava quando o tempo de transação padrão, que é 60 segundos, expirava no meio do processamento. Isso impactava também a parte das leituras que eram feitas na base de dados. O problema que eu classifiquei como "magia" poderia ser traduzido como uma descrição de erro não clara. Se logo no começo eu tivesse tomado um erro cuja descrição falasse algo de transação, ficaria mais claro para ir direto na raiz do problema. Mas fica a dica de que tudo tem explicação, só depende de quão fundo você pode e quer ir.

Em tempo, essa rotina que estava com problemas poderia ser melhor escrita. Ou seja, antes de iniciar a transação de gravação, ler e processar tudo o que fosse necessário, deixando apenas para gravar os dados no banco em um último momento e com transação somente nesta parte. Assim que possível vamos fazer isso.

[]'s