Desenvolvimento - ASP. NET

Por dentro da Base Classe Library - Capítulo 13 - Reflection

Esse capítulo propõe-se a explicar como criar esse tipo de funcionalidade dentro da aplicação.

por Israel Aéce



Introdução

Reflection (ou Reflexão) é a habilidade que temos em extrair e descobrir informações de metadados de um determinado Assembly. Os metadados descrevem os campos (propriedades, membros e eventos) de um tipo juntamente com seus métodos e, durante a compilação, o compilar gerará e armazenará metadados dentro do Assembly. São os metadados que permitem uma das maiores façanhas dentro da plataforma .NET, ou seja, escrevermos um componente em Visual C# e consumí-lo em uma aplicação em Visual Basic .NET.

Reflection não permite apenas extrair informações em runtime, mas também permitirá que se carreegar Assemblies, instancie classes, invoque seus métodos, etc.. Reflection é algo muito poderoso que existe e possibilita dar uma grande flexibilidade para a aplicação. O próprio .NET Framework utiliza Reflection internamente em diversos cenários, como por exemplo o Garbage Collector examina os objetos que um determinado objeto referencia para saber se o mesmo está ou não sendo utilizado. Além disso, quando serializamos um objeto, o .NET Framework utiliza Reflection para extrair todos os valores do membros internos do objeto para persistí-los. O próprio Visual Studio .NET utiliza informações extraídas via Reflection para habilitar o Intellisense e mais, quando está desenvolvendo um formalário e vai até a janela de propriedades de um determinado controle, o Visual Studio .NET extrai os membros do controle via Reflection para exibir e, conseqüentemente, alterar de acordo com a necessidade.

A idéia deste capítulo é apresentar como utilizar essa ferramenta poderosa que a Microsoft disponibilizou dentro do .NET Framework para extrair informações de metadados. Todas as classes que utilizaremos para Reflection estão contidas dentro do namespace System.Reflection e, na primeira parte do capítulo, veremos como é possível carregar Assemblies em memória, para em seguida, conseguir extrair as informações de classes, propriedades, métodos e eventos que um determinado tipo apresenta.

AppDomains

Historicamente, um processo é criado para isolar uma aplicação que está sendo executado dentro do mesmo computador. Cada aplicação é carregada dentro de um processo separado, que isola uma aplicação das outras. Esse isolamento é necessário porque os endereços são relativos à um determinado processo.

O código gerenciado passa por um processo de verificação que deve ser executado antes de rodar. Esse processo determina se o código pode acessar endereço de memória inválidos ou executar alguma outra ação que poderia causar alguma falha no processo. O código que passa por essa verificação é chamado de type-safe e permite ao CLR fornecer um grande nível de isolamento, como um processo, só que com muito mais performance.

AppDomain ou domínio de aplicação é um ambiente isolado, seguro e versátil que o CLR pode utilizar para isolar aplicações. Você pode rodar várias aplicações em um único processo Win32 com o mesmo nível de isolamento que existiria em um processo separado para cada uma dessas aplicações, sem ter o overhead que existe quando é necessário fazer uma chamado entre esses processos. Além disso, um AppDomain é seguro, já que o CLR garante que um AppDomain não conseguirá acessar os dados que estão contidos em outro AppDomain. Essa habilidade de poder rodar várias aplicações dentro de um único processo aumenta consideravelmente a escalabilidade do servidor.

Um AppDomain é criado para servir de container para uma aplicação gerenciada. A inicialização de um AppDomain consiste em algumas tarefas, como por exemplo, a criação da memória heap, onde todos os reference-types são alocados e de onde o lixo é coletado e a criação de um pool de threads, que pode ser utilizado por qualquer um dos tipos gerenciados que estão carregados dentro do processo.

O Windows não fornece uma forma de rodar aplicação .NET. Isso é feito a partir de uma CLR Host, que é uma aplicação responsável por carregar o CLR dentro de um processo, criando AppDomains dentro do mesmo e executando o código das aplicações que desenvolvemos dentro destes AppDomains.

Quando uma aplicação é inicializada, um AppDomain é criado para ela. Esse AppDomain também é conhecido como default domain e ele somente será descarregado quando o processo terminar. Sendo assim, o Assembly inicial rodará dentro deste AppDomain e, se desejar, você pode criar um novos AppDomains e carregar dentro destes outros Assemblies mas, uma vez carregado, você não pode descarregá-lo e isso somente acontecerá quando a AppDomain for descarregada.

O .NET Framework disponibiliza uma classe chamada AppDomain que representa e permite manipular AppDomains. Essa classe fornece vários métodos (alguns estáticos) que auxiliam desde a criação até o término de um AppDomain. Entre esses principais métodos, temos:

Método

Descrição

CreateDomain

Método estático que permite a criação de uma nova AppDomain.

CurrentDomain

Retorna um objeto do tipo AppDomain representando o AppDomain da thread corrente.

DoCallback

Executa um código em uma outra aplicação a partir de um delegate.

GetAssemblies

Retorna um array de objetos do tipo Assembly, onde cada elemento é um Assembly que foi carregado dentro do AppDomain.

IsDefaultAppDomain

Retorna uma valor boolano indicando se o AppDomain trata-se do AppDomain padrão.

Load

Permite carregar um determinado Assembly dentro do AppDomain.

Unload

Descarrega um determinado AppDomain.

Para exemplificar a criação e o descarregamento de um segundo AppDomain, vamos analisar o trecho código abaixo:

VB.NET

Imports System

Dim _domain As AppDomain = AppDomain.CreateDomain("TempDomain")

‘...

AppDomain.Unload(_domain)

C#

using System;

AppDomain _domain = AppDomain.CreateDomain("TempDomain");

//...

AppDomain.Unload(_domain);

Assemblies

Nomenclatura dos Assemblies

A nomenclatura de um Assembly (conhecida como display name ou identidade do Assembly) consiste em 4 informações: nome (sem “.exe” ou “.dll”), versão, cultura e a public key token (que será nula se uma strong name não for definida). Para exemplificar isso, vamos analisar o Assembly System.Data.dll da versão 2.0 do .NET Framework que é responsável pelas classes de acesso a dados e também um Assembly customizado chamado PeopleLibrary, onde não foi criado uma strong name:

System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

PeopleLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

O .NET Framework fornece uma classe chamada que descreve a identidade do Assembly, chamada AssemblyName. Entre as várias propriedades que essa classe possui, podemos destacar as mais importantes: CodeBase, CultureInfo, FullName, KeyPair, Name e Version.

Carregamento manual de Assemblies

Quando referenciamos um Assembly qualquer dentro de uma aplicação, o CLR decide quando carregá-lo. Quando você chama um método, o CLR verifica o código IL para ver quais tipos estão sendo referenciados e, finalmente, carrega os Assemblies onde os tais tipos estão sendo referenciados. Se um Assembly já estiver contido dentro do AppDomain, o CLR é inteligente ao ponto de conseguir identificar e não carregará o mesmo Assembly novamente.

Mas quando você quer extrair informações de metadados de um determinado Assembly, é necessário carregá-lo para dentro do seu AppDomain e, depois disso, poder extrair os tipos que ele expõe. Para que isso seja possível, o namespace System.Reflection possui uma classe chamada Assembly que possui, além de seus métodos de instância, alguns métodos estáticos que são utilizados para carregar um determinado Assembly. Entre esses métodos estáticos para carregamento do Assembly temos:

Método

Descrição

GetAssembly

Dado um determinado tipo, esse método consegue extrair o Assembly em qual esse tipo está contido.

GetCallingAssembly

Retorna um objeto do tipo Assembly que representa o Assembly onde o método está sendo invocado.

GetEntryAssembly

Retorna um objeto do tipo Assembly que está contido no AppDomain padrão (default domain).

GetExecutingAssembly

Retorna uma instância do Assembly onde o código está sendo executado.

Load

Um método com vários overloads que retorna um determinado Assembly. Um dos overloads mais comuns, é o que aceita uma string, conteudo o seu fully qualified name (nome, versão, cultura e token) como vimos acima.

Se o Assembly especificado conter com uma strong name, o CLR então procura dentro do GAC, seguido pelo diretório base da aplicação e dos diretórios privados da mesma. Agora, se o Assembly especificado não conter uma strong name, ele apenas não procurará dentro do GAC e, para ambos os casos, se o Assembly não for encontrado, uma exceção do tipo System.IO.FileNotFoundException.

LoadFile

Permite carregar um Assembly a partir do caminho fiísico até o mesmo, podendo carregar um Assembly de qualquer local que ele esteja, não resolvendo as dependências.

Esse método é utilizando em cenários mais limitados, onde não é possível a utilização do método LoadFrom, já que não permite carregar os Assemblies que tenham a mesma identidade, mas estão fisicamente em locais diferentes.

LoadFrom

Carrega um Assembly baseando-se no caminho físico, resolvendo todas as suas dependências (ver nota abaixo).

LoadWithPartialName

Carrega um Assembly do diretório da aplicação ou do GAC, mas trata-se de um método obsoleto e, ao invés dele, utilize o método Load.

Esse método não deve ser utilizado, porque você nunca sabe a versão do Assembly que irá carregar.

ReflectionOnlyLoad

Carrega um Assembly em um contexto reflection-only, ou seja, o Assembly poderá apenas ser examinado, mas não executado.

ReflectionOnlyLoadFrom

Dado o caminho físico até o Assembly, o mesmo é carregado dentro do domínio do chamador, também em um contexto reflection-only.

Nota Importante: Vamos imaginar que temos uma aplicação Windows Forms (EXE) e uma biblioteca (DLL) que essa aplicação Windows utiliza. A estrutura é a seguinte:

C:\Program Files\PeopleUI\WinUI.exe

C:\Program Files\PeopleUI\PeopleLibrary.dll

Como podemos ver, a aplicação Windows (WinUI.exe) foi desenvolvida referenciando a biblioteca PeopleLibrary.dll. Imagine que, dentro do método Main ele carrega a seguinte DLL: “C:\PeopleLibrary.dll” através do método LoadFrom da classe Assembly. Imagine que, depois de carregado o mesmo Assembly, mas de um outro local, ao invocar um método dentro do PeopleLibrary.dll ele invocará de qual dos dois locais (“C:\” ou “C:\Program Files\PeopleUI\”)? Como o CLR não pode supor que os Assemblies são iguais somente porque o nome do arquivo coincide, felizmente ele sabe qual deverá executar. Quando o método LoadFrom é executado, o CLR extrai informações sobre a identidade do Assembly (versão, cultura e token), passando essas informações para o método Load que, faz a busca pelo Assembly. Se um Assembly correspondente for encontrado, o CLR fará a comparação do caminho do arquivo especifico no método LoadFrom e do caminho encontrado pelo método Load. Se os caminhos forem idênticos, o Assembly será considerado parte da aplicação, caso contrário, será considerado um “arquivo de dados”.

Sempre que possível, é bom evitar o uso do método LoadFrom e opte por utilizar o método Load. A razão para isso é que, internamente, o método LoadFrom invoca o método Load. Além disso, o LoadFrom trata o Assembly como um “arquivo de dados” e, se o CLR carrega dentro de um AppDomain o mesmo Assembly a partir de caminhos diferentes, uma grande quantidade de memória é desperdiçada.

Exemplo: O código abaixo exemplifica a utilização do método LoadFrom da classe Assembly. O trecho de código foi extraído de uma demonstração e, você pode consultar na íntegra no seguinte local: XXXXXXXXXXXXXXXXXXXXXX.

VB.NET

Imports System

Imports System.Reflection

Dim asb As Assembly = Assembly.LoadFrom("C:\Comp.dll")

C#

using System;

using System.Reflection;

Assembly asb = Assembly.LoadFrom("C:\\Comp.dll");

Trabalhando com Metadados

Como já vimos anteriormente, os metadados são muito importantes dentro da plataforma .NET. Vimos também como criar AppDomains e carregar Assemblies dentro deles. Somente carregar os Assemblies dentro de um AppDomain não tem muitas utilidades.

Uma vez que ele encontra-se carregado, é perfeitamente possível extrair informações de metadados dos tipos que estão contidos dentro Assembly e, para isso, utilizaremos várias classes que estão contidas dentro do namespace System.Reflection. A partir de agora analisaremos essas classes que estão disponíveis para a criação e manipulação de tipos, como por exemplo, invocar métodos, recuperar ou definir valores para propriedades, etc..

Antes de mais nada, precisamos entender qual a hierarquia dos objetos que estão disponíveis para a manipulação dos metadados e para invocá-los dinamicamente. A imagem abaixo exibe tal hierarquia onde, como já era de se esperar, o ancestral comum é o System.Object.

Imagem 15.1 – Hierarquia das classes para manipulação de metadados

System.Reflection.MemberInfo

MemberInfo é uma classe abstrata que é base para todas as classes utilizadas para resgatar informações sobre os membros sejam eles construtores, eventos, campos, métodos ou propriedades de uma determinada classe. Basicamente essa classe fornece as funcionalidades básicas para todos as classes que dela derivarem. Os membros desta classe abstrata são:

Membro

Descrição

Name

Retorna uma string representando o membro.

MemberType

Propriedade de somente leitura que retorna um item do enumerador MemberTypes indicando qual tipo de membro ele é. Entre os itens deste enumerador, temos:

    Parâmetro

Descrição

typeName

Uma string representando o tipo que deverá ser instanciado.

ignoreCase

Um valor booleano indicando se a busca pelo tipo deverá ou não ser case-sensitive.

bindingAttr

Uma combinação de valores disponibilizados no enumerador BindingFlags que afetará na forma que a busca pelo tipo será efetuada. Entre os valores disponibilizados por esse enumerador, temos os principais deles descritos abaixo:

  • CreateInstance – Especifica que o Reflection deverá criar uma instância do tipo especificado e chama o construtor que se enquandra com o array de System.Objects que pode ser passado para o método CreateInstance.
  • DeclaredOnly – Somente membros declarados no mesmo nível da hierarquia do tipo será considerado na busca por membros e tipos.
  • Default – Especifica o flag de no-binding.
  • ExactBinding – Esta opção especifica que os tipos dos argumentos fornecidos devem exatamente coincidir com os tipos correspondentes dos parâmetros. Uma Exception é lançada se o chamador informar um Binder.
  • FlattenHierarchy – Especifica que membros estáticos públicos e protegidos devem ser retornados. Membros estáticos privados não são retornados. Membros estáticos incluem campos, métodos, eventos e propriedades. Tipos aninhados não são retornados.
  • GetField – Espefica que o valor de um campo especifíco deve ser retornado.
  • GetProperty –Especifica que o valor de uma propriedade específica deve ser retornada.
  • IgnoreCase – Especifica que o case-sensitive deve ser considerado quando efetuar o binding.
  • IgnoreReturn – Usada em interoperabilidade com COM, ignora o valor de retorno do método.
  • Instance – Especifica que membros de instância são incluídos na pesquisa do membro.
  • InvokeMethod – Especifica que o método será invocado.
  • NonPublic – Especifica que membros não-públicos serão incluídos na pesquisa do membro.
  • OptionalParamBinding – Esta opção é utilizada quando o método possui parâmetros default.
  • Public – Especifica que membros públicos serão incluídos na pesquisa de membros e tipos.
  • SetField – Este valor especifica que o valor de um membro específico deve ser definido.
  • SetProperty – Espeifica que o valor de uma propriedade específica deve ser definido.
  • Static – Especifica que membros estáticos serão incluídos na pesquisa do membro.

binder

Um objeto que habilita o binding, coerção de tipos do argumentos, invocação dos membros e recuperar a instância de objetos MemberInfo via Reflection. Se nenhum binder for informado, um binder padrão é utilizado.

args

Um array de System.Object contendo os argumentos que devem ser passados para o construtor. Se o construtor for sobrecarregado, a escolha do qual utilizar será feito a partir do número de tipo dos parâmetros informados quando invocar o objeto.

culture

Uma instância da classe CultureInfo que é utilizada para a coerção de tipos. Se uma referência nula for passado para este parâmetro, a cultura da thread corrente será utilizada.

activationAttributes

Um array de elementos do tipo System.Object contendo um ou mais atributos que podem ser utilizados durante a ativação do objeto.

Se adicionarmos um construtor na classe Cliente, então devemos passar ao parâmetro args um array com os valores para satisfazer o overload, onde é necessário estar com o mesmo número de parâmetros, ordem e tipos. Para fins de exemplo, vamos, através do código abaixo, invocar a classe Cliente passando os objetos para o construtor que aceita um inteiro e uma string. O código muda ligeiramente:

VB.NET

Imports System.Reflection

Dim asb As Assembly = Assembly.LoadFrom("C:\PeopleLibrary.dll")

Dim constrArgs() As Object = {123, "José Torres"}

Dim cliente As Object = asb.CreateInstance( _

"PeopleLibrary.Cliente", _

False, _

BindingFlags.CreateInstance, _

Nothing, _

constrArgs, _

Nothing, _

Nothing)

C#

using System.Reflection;

Assembly asb = Assembly.LoadFrom(@"C:\PeopleLibrary.dll");

object[] constrArgs = { 123, "José Torres" };

object cliente = asb.CreateInstance(

"PeopleLibrary.Cliente",

false,

BindingFlags.CreateInstance,

null,

constrArgs,

null,

null);

O método CreateInstance retorna um System.Object representando a instância da classe informada. Se caso o tipo não for encontrado, o método retorna uma valor nulo e, sendo assim, é necessário testar essa condição para não deixar a aplicação falhar. Em seguida, depois do objeto devidamente instanciado, vamos analisar como fazer para invocar os métodos e propriedades que o objeto possui. Para que possamos invocar os tipos do objeto, é necessário extrairmos o Type do mesmo como já vimos acima e, dando seqüência ao exemplo, as próximas linhas de código ilustram como extrair o objeto Type, através do método GetType da variável cliente:

VB.NET

Dim tipo As Type = cliente.GetType()

C#

Type tipo = cliente.GetType();

Essa classe tem um método denominado ExibeDados, que retorna uma string com o Id e o Nome do cliente concatenados. Como definimos no construtor da classe os valores 123 e “José Torres”, ao invocar o método ExibeDados, esses valores deverão ser exibidos. Para que seja possível invocar o método em runtime, necessitamos utilizar o método InvokeMember da classe Type, que retorna um System.Object com o valor retornado pelo método. O exemplo abaixo ilustra como utilizá-lo:

VB.NET

Console.WriteLine(tipo.InvokeMember( _

"ExibeDados", _

BindingFlags.InvokeMethod, _

Nothing, _

cliente, _

Nothing))

C#

Console.WriteLine(tipo.InvokeMember(

"ExibeDados",

BindingFlags.InvokeMethod,

null,

cliente,

null));

Entre os parâmetros que esse método utiliza, temos (na ordem em que aparecem no código acima): uma string contendo o nome do construtor, método, propriedade ou campo a ser invocado (se informar uma string vazia, o membro padrão será invocado); uma combinação das opções fornecidas pelo enumerador BindingFlags (detalhado mais acima) indicando como a busca pelo membro será efetuada; binder a ser utilizado para a pesquisa do membro; a instância do objeto de onde o método será pesquisado e executado e, finalmente, um array de elementos do tipo System.Object, contendo os parâmetros necessários para ser passado para o método a ser executado.

Além do método, ainda há a possibilidade de invocarmos propriedades em runtime. Podemos além de ler as informações de cada uma delas, podemos definir os valores a elas. Para isso, utilizamos o método GetProperty da classe Type, que retorna uma instância da classe PropertyInfo, que representa a propriedade e, através dela, definimos os valores e extraimos para escrevê-los, assim como é mostrado no trecho de código abaixo:

VB.NET

Dim nome As PropertyInfo = tipo.GetProperty("Nome")

Dim id As PropertyInfo = tipo.GetProperty("Id")

nome.SetValue(cliente, "Mario Oliveira", Nothing)

id.SetValue(cliente, 456, Nothing)

Console.WriteLine(nome.GetValue(cliente, Nothing))

Console.WriteLine(id.GetValue(cliente, Nothing))

C#

PropertyInfo nome = tipo.GetProperty("Nome");

PropertyInfo id = tipo.GetProperty("Id");

nome.SetValue(cliente, "Mario Oliveira", null);

id.SetValue(cliente, 456, null);

Console.WriteLine(nome.GetValue(cliente, null));

Console.WriteLine(id.GetValue(cliente, null));

Basicamente, quando chamamos o método SetValue, passamos a instância do objeto onde as propriedades serão manipuladas; o novo valor a ser definido para a propriedade e, finalmente, um objeto do tipo System.Object, quando a propriedade se tratar de uma propriedade indexada. Já o método GetValue é quase idêntico, apenas não temos o valor a ser definido, pois como o próprio nome do método diz, ele é utilizado para ler o conteúdo da propriedade.

Finalmente, se quisermos extrair os eventos que a classe possui e vincularmos dinamicamente para que o mesmo seja disparado, podemos fazer isso através do método AddEventHandler fornecido pelo classe EventInfo. Como sabemos, a classe Cliente fornece um evento chamado AlterouDados, qual podemos utilizar para que quando um valor for alterado em uma das propriedades, esse evento seja disparado. O código abaixo ilustra como configurar para vincular dinamicamente o evento:

VB.NET

Dim ev As EventInfo = tipo.GetEvent("AlterouDados")

ev.AddEventHandler(cliente, New EventHandler(AddressOf Teste))

‘...

Private Sub Teste(ByVal sender As Object, ByVal e As EventArgs)

Console.WriteLine("Alterou...")

End Sub

C#

EventInfo ev = tipo.GetEvent("AlterouDados");

ev.AddEventHandler(cliente, new EventHandler(Teste));

//...

private void Teste(object sender, EventArgs e)

{

Console.WriteLine("Alterou...");

}

Criação dinâmica de Assemblies

Há um namespace dentro de System.Reflection chamado de System.Reflection.Emit. Dentro deste namespace existem várias classes que são utilizadas para criarmos dinamicamente um Assembly e seus respectivos tipos.

Essas classes são também conhecidas como builder classes, ou seja, para cada um dos membros que vimos anteriormente, como por exemplo, Assembly, Type, Constructor, Event, Property, etc., existem uma classe correspodente com um sufixo em seu nome, chamado XXXBuilder, indicando que é um construtor de um dos itens citados. Para a criação de um Assembly dinâmico, temos as seguintes classes:

Classe

Descrição

ModuleBuilder

Cria e representa um módulo.

EnumBuilder

Representa um enumerador.

TypeBuilder

Fornece um conjunto de rotinas que são utilizados para criar classes, podendo adicionar métodos e campos.

ConstructorBuilder

Define um construtor para uma classe.

EventBuilder

Define um evento para uma classe.

FieldBuilder

Define um campo.

PropertyBuilder

Define uma propriedade para uma determinada classe.

MethodBuilder

Define um método para uma classe.

ParameterBuilder

Define um parâmetro.

GenericTypeParameterBuilder

Define um parâmetro genérico para classes e métodos.

LocalBuilder

Cria uma variável dentro de um método ou construtor.

ILGenerator

Gera código MSIL (Microsoft Intermediate Language).

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.