
La reflection è una funzionalità del .NET Framework che ci permette di ispezionare e manipolare a runtime i metadati e il codice compilato in un assembly .NET. È una feature estremamente potente, ma, come tutti gli strumenti a nostra disposizione, bisogna capire come funziona per poterla usare nel migliore dei modi.
Gli assembly forniscono al Common Language Runtime le informazioni necessarie per riconoscere le implementazioni dei tipi. Un assembly può essere considerato come una raccolta di tipi e risorse che formano un’unità logica di funzionalità e che sono creati per interagire. Sono costituiti da una o più unità indipendenti chiamate Moduli. Le informazioni di nostro interesse sono contenute nel modulo principale e sono le seguenti:
- l’Assembly Manifest, che a sua volta contiene il nome dell’Assembly, la versione e un catalogo di tutti i moduli contenuti al suo interno;
- I Metadati, che contengono una descrizione completa di tutti i tipi esposti, compresi metodi, campi, parametri e costruttori, eventuali riferimenti ad assembly esterni e l’IL (Intermediate Language) che è stato compilato dal codice sorgente ed è l’effettiva parte eseguibile dell’assembly.
Un assembly, precedentemente compilato in IL (Intermediate Language), viene compilato in linguaggio macchina dal compilatore JIT (Just in Time Compiler), detto anche JITter. Il codice nativo viene prodotto quando richiesto cioè, ad esempio, alla prima invocazione di un metodo esso viene compilato e conservato in memoria. Alle successive chiamate, sarà già disponibile in cache e di conseguenza sarà risparmiato il tempo di compilazione. Se non sono usati, gli assembly non vengono caricati. Questo significa che rappresentano un modo efficace per gestire le risorse nei progetti di maggiori dimensioni. Il vantaggio della compilazione JIT è che può essere realizzata dinamicamente in modo da trarre vantaggio dalle caratteristiche del sistema sottostante.
Attraverso l’utilizzo dei Metadati, possiamo creare dinamicamente istanze di tipi, invocare metodi, ottenere ed impostare i valori di proprietà e campi, o ispezionare attributi.
C# ci offre classi e metodi per eseguire queste e molte altre cose, utilizzando quella che viene chiamata Reflection.
Le classi, che ci interessano maggiormente per l’utilizzo della Reflection, sono tre:
Classe Type
La classe Type è una classe astratta che utilizzeremo per accedere ai metadati attraverso le classi della Reflection. Utilizzando un oggetto Type è possibile ottenere informazioni relative a costruttori, metodi, campi, proprietà ed eventi di una classe.
Classe Activator
Questa classe contiene metodi che possono essere utilizzati per creare dinamicamente un’istanza di un tipo.
Classe Assembly
Questa classe astratta ci permette di caricare, manipolare, ottenere informazioni su un assembly e individuare tutti i tipi in esso contenuti.
Vediamo un piccolo esempio pratico, utilizzando la Reflection e gli attributi personalizzati per mappare le proprietà degli oggetti.
Nonostante Entity Framework e il namespace System.ComponentModel.DataAnnotations ci forniscono già strumenti per poterlo fare, potremmo aver bisogno di costruire il nostro livello o strumento di accesso ai dati.
Potremmo implementare la nostra versione di queste annotazioni di dati come Attributi Personalizzati. Per creare un attributo personalizzato in C#, creiamo semplicemente una classe che eredita da System.Attribute. Ad esempio, se volessimo implementare un nostro attributo [PrimaryKey] per indicare che una particolare proprietà su una classe nella nostra applicazione rappresenta la chiave primaria nel nostro database, potremmo creare il seguente attributo personalizzato:
public class PrimaryKeyAttribute : Attribute { }
Gli attributi possono anche trasmettere informazioni, se necessario. Consideriamo un modo per mappare la proprietà su uno specifico nome di colonna del database.
Creiamo una classe che eredita da System.Attribute, ma questa volta aggiungiamo una proprietà e un costruttore:
public class DbColumnAttribute : Attribute
{
public string Name { get; set; }
public DbColumnAttribute(string name)
{
this.Name = name;
}
}
Ora, supponiamo di ereditare un database che dobbiamo integrare con il nostro codice esistente. La tabella da cui verranno ricavate le informazioni sul client utilizza tutti i nomi delle colonne minuscole e separate da un underscore, per meglio comprendere il contesto. Di seguito, il codice SQL della creazione della tabella in questione:
CREATE TABLE Users (
id_user int IDENTITY(1,1) PRIMARY KEY NOT NULL,
first_name varchar(50) NOT NULL,
last_name varchar(50) NOT NULL,
email varchar(50) NOT NULL
);
Ed ecco come apparirà la nostra classe Utente con gli attributi di nome colonna:
public class User
{
[PrimaryKey]
[DbColumn("id_user")]
public int UserId { get; set; }
[DbColumn("first_name")]
public string Name { get; set; }
[DbColumn("last_name")]
public string Surname { get; set; }
[DbColumn("email")]
public string Email { get; set; }
}
Potremmo ora scrivere un metodo che ci permetta, utilizzando la Reflection, di mappare le proprietà della nostra classe sullo specifico nome di colonna del database:
static void DbColumnsMappingMethod<T> (T obj) where T : new()
{
// Get the istance's Type object
Type type = obj.GetType();
// Get a list of the public properties of the Type object as PropertyInfo objects
PropertyInfo[] objectPropertiesList = type.GetProperties();
Console.WriteLine("Search properties for the object: {0}", type.Name);
foreach (var objectProperty in objectPropertiesList)
{
// Get custom attributes of object's properties
var customAttributes = objectProperty.GetCustomAttributes(false);
string message = "The property {0} will be mapped with the database column {1}";
// Mapping custom attributes with DB columns
var mappedColumn = customAttributes
.FirstOrDefault(a => a.GetType() == typeof(DbColumnAttribute));
if (mappedColumn != null)
{
DbColumnAttribute dbColumn = mappedColumn as DbColumnAttribute;
Console.WriteLine(message, objectProperty.Name, dbColumn.Name);
}
}
}
Potremmo anche scrivere un metodo che, utilizzando la Reflection, esamini le proprietà e gli attributi di un oggetto e ne ricerchi la chiave primaria:
static void PrimaryKeySearchMethod<T> (T obj) where T : new()
{
// Get the istance's Type object
Type type = obj.GetType();
// Get a list of the public properties of the Type object as PropertyInfo objects
PropertyInfo[] objectPropertiesList = type.GetProperties();
Console.WriteLine("Search Primary Key for the object: {0}", type.Name);
// Primary Key search
var primaryKey = objectPropertiesList
.FirstOrDefault(p => p.GetCustomAttributes(false)
.Any(a => a.GetType() == typeof(PrimaryKeyAttribute)));
if (primaryKey != null)
{
string message = "The Primary Key for {0} class is the property: {1}";
Console.WriteLine(message, type.Name, primaryKey.Name);
}
}
Nel codice sopra, passiamo al metodo un oggetto generico di tipo T, il che significa che questo metodo può essere utilizzato con qualsiasi oggetto di dominio per verificare la presenza di un attributo [PrimaryKey] .
Innanzitutto, utilizziamo il metodo GetType() per trovare le informazioni sul Type dell’oggetto, quindi chiamiamo il metodo GetProperties() dell’istanza Type, che restituisce un array di oggetti PropertyInfo.
Successivamente, eseguiamo l’iterazione tramite LINQ su ciascuna delle istanze PropertyInfo e chiamiamo il metodo GetCustomAttributes() , che restituirà una matrice di oggetti che rappresentano i CustomAttributes trovati su quella proprietà. Possiamo quindi controllare il tipo di ciascun oggetto CustomAttribute e, se è di tipo PrimaryKeyAttribute, sappiamo che abbiamo trovato una proprietà che rappresenta una chiave primaria nel nostro database.
Dobbiamo precisare che l’uso della Reflection può rendere il nostro codice meno performante, ma vista la potenza dei processori odierni, questa cosa si noterebbe solo se il codice venisse utilizzato ripetutamente in un contesto di applicazione molto grande. Potremmo mitigare questo problema utilizzando ad esempio astrazioni piuttosto che tipi concreti, best practice che in genere si traduce nell’utilizzo di un’interfaccia invece di una classe concreta. Utilizzando un’interfaccia, possiamo avere gran parte del nostro codice che interagisce con l’astrazione piuttosto che con il tipo creato dinamicamente.
Se ne avessimo bisogno, potremmo per esempio caricare dinamicamente un assembly una sola volta, all’avvio dell’applicazione; caricare dinamicamente i tipi in esso contenuti, assicurandoci ancora una volta di caricarli una volta sola; ottenuto un tipo potremmo utilizzarlo tramite un’interfaccia che la nostra applicazione userà per effettuare tutte le chiamate ai metodi: in questo modo non ci saranno chiamate dinamiche attraverso MethodInfo e il metodo Invoke.
Questo semplice esempio di strategia, ci aiuterà a ridurre il calo di prestazioni e, poiché le interfacce hanno solo membri pubblici, eviteremo anche di interagire coi membri privati della classe. Il risultato è che limiteremo l’uso della Reflection solo alle parti dell’applicazione dove ne abbiamo davvero bisogno, massimizzando le PRESTAZIONI pur mantenendo la FLESSIBILITÀ che vogliamo.
Alla prossima!