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éceIntroduçã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:
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:
|
|
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). |