Páginas

segunda-feira, 26 de outubro de 2009

GAC vs. <codeBase>

Onde trabalho, não faz muito tempo que fomos questionados sobre boas práticas e o bom uso de assemblies .NET no GAC (Global Assembly Cache). Sinceramente, o melhor uso do GAC é não utilizá-lo, pelo menos no contexto que as aplicações aqui são desenvolvidas.

As aplicações, com excessão das que são utilitários e frameworks, geralmente são isoladas no que se diz respeito a reaproveitamento de código. Então, o uso do GAC não se justifica para essa maioria dos casos.

Umas das coisas legais que aprendi enquanto dava uma olhada no assunto foi utilizar melhor as configurações de runtime, mais especificamente em relação ao codebase.

Com esta configuração, é possível indicar o caminho de um assembly que deve ser utilizado pela aplicação, estando ela onde estiver na máquina ou inclusive na rede! Com isso, acabou a desculpa de colocar um componente no GAC apenas porque a aplicação, seja ela windows ou web, não consegue encontrá-lo. GAC é para compartilhar, como o próprio nome diz.

E como fazer com que o runtime encontre o assembly? Simplesmente adicionando a seguinte configuração ao arquivo config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="CodebaseLib"
                                    publicKeyToken="72eb1259ccc328b1"
                                    culture="neutral" />
                <codeBase version="1.0.0.0"
                          href="file://D:\Projetos\Testes\TesteCodebase\CodebaseLib\bin\Debug\CodebaseLib.dll"/>
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

Cada assembly que precisa ser encontrado deve ter uma tag dependentAssembly, contendo as tags assemblyIdentity e codeBase. A própria configuração é auto-explicativa: com assemblyIdentity, se identifica o componente, informando o nome, a cultura, o publicKeyToken (no caso de componentes com strong name); codeBase informa a versão e onde encontrar o componente.

Aqui cabem duas observações:

  1. href pode fazer referência tanto a um arquivo em disco como um arquivo na internet (poderia ser http://www.meusite.com.br/CodebaseLib.dll).
  2. Caso o componente tenha strong name, então ele pode se localizar em qualquer lugar; já se ele não possuir então deve estar em um subdiretório do diretório da aplicação.

[]’s

sexta-feira, 16 de outubro de 2009

Detection of product '{0}', feature 'Base_And_Client', component '{1}' failed

Finalmente consegui resolver o problema que estava tendo com o Windows Installer e o COM+ de uma forma aceitável.

O Problema

O problema inicial foi quando criei um instalador, utilizando o InstallShield, para empacotar um aplicativo que utilizamos onde trabalho, que havia sido migrado de .NET 1.1 para .NET 2.0. Este aplicativo é composto de componentes .NET 1.1 (para manter a compatibilidade com versões de sistemas que usam esse runtime), .NET 2.0 com windows services e COM+ (também para manter compatibilidade).

A compatibilidade foi necessária para fazer com que os sistemas que utilizassem a versão em .NET 1.1 desse aplicativo pudessem passar a utilizar a nova, sem a necessidade de alterações em código. E para manter a compatibilidade, foi criado o pacote COM+ citado acima, para ser o divisor de águas entre os runtimes 1.1 e 2.0, permitindo as chamadas.

Quando este pacote foi instalado nos servidores de componentes, a cada chamada do aplicativo, eram logados os eventos abaixo, além de ter um pequeno atraso na execução do mesmo.



Detection of product '{3FAE8AAC-EA51-4060-8181-7FC647E5EC9E}', feature 'Base_And_Client', component '{5A249606-8811-4E9D-BF61-36E04087B112}' failed. The resource '' does not exist.

Detection of product '{3FAE8AAC-EA51-4060-8181-7FC647E5EC9E}', feature 'Base_And_Client' failed during request for component '{3E87C314-FFBB-4F7F-A52C-609189873BB3}'
O produto {3FAE8AAC-EA51-4060-8181-7FC647E5EC9E} é o aplicativo que foi migrado para .NET 2.0. O componente {3E87C314-FFBB-4F7F-A52C-609189873BB3} é o componente que fica no COM+, responsável por rotear as chamadas vindas do .NET 1.1 para .NET 2.0 e o componente {5A249606-8811-4E9D-BF61-36E04087B112} é o componente que fez a chamada ao componente do COM+. Ambos também são instalados no GAC.

Obs. Utilizar COM+ para fazer a interoperabilidade entre as versões do runtime do .NET talvez não tenha sido a melhor idéia. Poderia ter utilizado .NET Remoting ou mesmo Web Services para isso, mas esse questionamento fica para depois.

A Análise

Como introduzimos o instalador MSI para empacotar o aplicativo, minha desconfiança caiu sobre o Windows Installer. Após algumas pesquisas, descobri que existe uma funcionalidade no Windows Installer chamada “resiliency” (comentei sobre ela neste post http://galorebr.blogspot.com/2009/09/windows-installer-application.html). Basicamente ela restaura uma instalação caso um componente não seja encontrado.

Parecia que a CLR do .NET 1.1 não encontrava o componente COM+ em .NET 2.0, mesmo estando no GAC. Talvez isso seja porque existem diferenças de localização do GAC entre essas versões do runtime: a primeira coloca as dll’s em C:\WINDOWS\assembly\GAC, a segunda em C:\WINDOWS\assembly\GAC_MSIL.

Utilizando o Filemon (http://technet.microsoft.com/en-us/sysinternals/bb896642.aspx), não encontrei evidências de não se encontrar o arquivo.
A próxima análise foi utilizando o log do Fusion (http://blogs.msdn.com/suzcook/archive/2003/05/29/57120.aspx). Com isso, conseguimos identificar que o CLR tomava um capote quando tentava achar o componente do COM+. Um detalhe que percebemos é que o erro acontecia quando o sistema que chamava nosso aplicativo se encontrava no COM+ também (.NET 1.1). Um dos locais onde era pesquisada a existência da dll era o diretório C:\Windows\System32\.

Como o CLR não encontrava a dll, chamava o Windows Installer para perguntar por que ela não existia (http://msdn.microsoft.com/en-us/library/15hyw9x3(VS.71).aspx, If Assembly2 is not found at either of those locations, the runtime queries the Windows Installer ). Isso é que provavelmente ocasionava a tentativa de reparação.

A Solução

Talvez não seja a solução ideal, nem a mais bela, mas funcionou. Simplesmente colocamos uma cópia do componente que vai no COM+ dentro do diretório C:\Windows\System32\. Com isso, o CLR localiza o componente e não pergunta nada ao Windows Installer.

Somente uma ressalva, não fizemos testes quando os sistemas que chamam nosso aplicativo possuem a propriedade Application Root Directory ajustada no COM+. Com isso, teoricamente o CLR não iria procurar no System32. Mas se isso acontecer, podemos ajudar no arquivo de config desse sistema para localizar o componente no lugar certo, através do Probing (http://msdn.microsoft.com/en-us/library/4191fzwb(VS.71).aspx.

Ufa, finalmente este problema foi resolvido!

[]’s