Un framework ORM (Object-Relational Mapping) sert d'intermédiaire entre une base de données et les classes représentant les entités de votre application. Il facilite l'interaction avec la base de données, gérant la synchronisation entre les structures de données de la base et les objets de votre code. Cette approche améliore considérablement l'efficacité du développement en éliminant la nécessité de coder manuellement les couches d'accès aux données.
Compréhension des Génériques et de la Réflexion
La réflexion permet à une application d'examiner et de modifier sa propre structure et son comportement pendant l'exécution. Elle peut être utilisée pour interroger les méthodes d'une classe, instancier des types dynamiquement et accéder aux métadonnées d'un assembly (fichier DLL ou EXE). Lors de la compilation, le code C# est transformé en langage intermédiaire (IL) et accompagné de métadonnées. La réflexion exploite ces informations pour permettre une introspection dynaimque.
Exemples de base de la réflexion :
Chargement d'un Assembly :
// Charger un assembly par son nom complet (incluant l'extension)
System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom("MonAssembly.dll");
// Charger un assembly par son chemin d'accès complet
// System.Reflection.Assembly assemblyWithPath = System.Reflection.Assembly.LoadFile(@"C:\Chemin\Vers\MonAssembly.dll");
// Charger un assembly par son nom (sans extension)
// System.Reflection.Assembly assemblyByName = System.Reflection.Assembly.Load("MonAssembly");
Obtention d'un Type Spécifique :
// Obtenir un type spécifique à partir de l'assembly chargé
// Le nom complet du type (incluant l'espace de noms) est requis
System.Type typeDeMaClasse = assembly.GetType("MonNamespace.MaClasse");
Création d'une Instance :
// Créer une instance du type obtenu
object instanceObjet = Activator.CreateInstance(typeDeMaClasse);
// Une alternative pour créer une instance en spécifiant le nom de l'assembly et du type
// object instanceAlternative = Activator.CreateInstance("MonAssembly.dll", "MonNamespace.MaClasse");
// Pour appeler des méthodes de manière dynamique sans vérification à la compilation,
// utilisez le mot-clé 'dynamic'. Notez que cela contourne la sécurité de type de C#.
// dynamic instanceDynamique = Activator.CreateInstance(typeDeMaClasse);
// instanceDynamique.MaMethode(); // Appel dynamique d'une méthode
Conversion de Type :
// Conversion de type sécurisée : retourne null si la conversion échoue
// IDbOperation operation = instanceObjet as IDbOperation;
// if (operation != null) { /* Utiliser operation */ }
// Conversion explicite (peut lever une exception si les types ne correspondent pas)
// MaClasse instanceReelle = (MaClasse)instanceObjet;
Appel de Méthode :
// Supposons que 'operation' est de type IDbOperation et que la conversion a réussi
// operation.ExecuterRequete();
Bien que la réflexion soit puissante, cet article se concentre sur l'utilisation de la classe Type pour l'introspection.
Définition de l'Interface
Pour un composant d'accès aux données, les opérations CRUD (Create, Read, Update, Delete) sont fondamentales. Un ORM devrait également permettre de générer des tables à partir de classes ou vice-versa. La génération de tables à partir de classes semble plus simple à implémenter pour un exemple basique. L'interface suivante définit les opérations principales :
public abstract class EntiteBaseAvecId
{
protected EntiteBaseAvecId() { } // Constructeur par défaut requis pour Activator.CreateInstance
protected EntiteBaseAvecId(int id)
{
Id = id;
}
public int Id { get; set; }
}
public interface IOperationAccesDonnees<TEntite> where TEntite : EntiteBaseAvecId, new()
{
List<TEntite> RecupererTous();
List<TEntite> RecupererParId(int id);
List<TEntite> RecupererSelonCritere(Func<TEntite, bool> critere);
bool MettreAJour(TEntite entite);
bool CreerEnregistrement(TEntite entite);
bool CreerTable();
bool Supprimer(int id);
}
Implémentation pour MySQL
L'exemple suivant utilise MySQL. Les principes peuvent être adaptés pour d'autres systèmes de gestion de base de données en modifiant les commandes SQL et les fournisseurs de données ADO.NET correspondants.
using MySql.Data.MySqlClient;
using System.Reflection;
public class GestionnaireMySql<TEntite> : IOperationAccesDonnees<TEntite>
where TEntite : EntiteBaseAvecId, new()
{
private readonly string _chaineConnexion = "Server=localhost;UserID=root;Password=votre_mot_de_passe;Database=votre_db;";
/// <summary>
/// Exécute une requête SQL de lecture et retourne une liste d'entités.
/// </summary>
/// <param name="sql">La requête SQL à exécuter.</param>
/// <returns>Une liste d'entités du type TEntite.</returns>
private List<TEntite> ExecuterRequeteLecture(string sql)
{
var resultats = new List<TEntite>();
using (var connexion = new MySqlConnection(_chaineConnexion))
{
connexion.Open();
var commande = new MySqlCommand(sql, connexion);
using (var lecteur = commande.ExecuteReader())
{
while (lecteur.Read())
{
TEntite element = new TEntite();
foreach (PropertyInfo propriete in typeof(TEntite).GetProperties())
{
// Gérer les valeurs NULL de la base de données
object valeur = lecteur[propriete.Name];
if (valeur != DBNull.Value)
{
propriete.SetValue(element, valeur);
}
}
resultats.Add(element);
}
}
}
return resultats;
}
/// <summary>
/// Convertit le type d'une propriété C# en type SQL correspondant pour la création de table.
/// </summary>
/// <param name="proprieteInfo">Informations sur la propriété.</param>
/// <returns>Le type SQL correspondant.</returns>
private string ConvertirTypeProprieteVersSql(PropertyInfo proprieteInfo)
{
return proprieteInfo.PropertyType.Name switch
{
"Int32" => "INT",
"String" => "VARCHAR(255)",
"Double" => "DOUBLE",
"Decimal" => "DECIMAL(10, 2)",
"DateTime" => "DATETIME",
_ => "TEXT" // Type par défaut pour les types non gérés
};
}
/// <summary>
/// Exécute une requête SQL de modification (INSERT, UPDATE, DELETE).
/// </summary>
/// <param name="sql">La requête SQL à exécuter.</param>
/// <returns>True si l'opération a réussi, False sinon.</returns>
private bool ExecuterRequeteModification(string sql)
{
using (var connexion = new MySqlConnection(_chaineConnexion))
{
connexion.Open();
var commande = new MySqlCommand(sql, connexion);
int lignesAffectees = commande.ExecuteNonQuery();
return lignesAffectees > 0;
}
}
public List<TEntite> RecupererTous()
{
string nomTable = typeof(TEntite).Name;
string sql = $"SELECT * FROM {nomTable}";
return ExecuterRequeteLecture(sql);
}
public List<TEntite> RecupererParId(int id)
{
string nomTable = typeof(TEntite).Name;
// Note: Une approche plus robuste utiliserait des paramètres pour éviter l'injection SQL
string sql = $"SELECT * FROM {nomTable} WHERE Id = {id}";
return ExecuterRequeteLecture(sql);
}
public List<TEntite> RecupererSelonCritere(Func<TEntite, bool> critere)
{
string nomTable = typeof(TEntite).Name;
string sql = $"SELECT * FROM {nomTable}";
// Le filtrage côté client après récupération de tous les enregistrements
return ExecuterRequeteLecture(sql).Where(critere).ToList();
}
public bool MettreAJour(TEntite entite)
{
string nomTable = typeof(TEntite).Name;
var colonnesValeurs = new List<string>();
foreach (PropertyInfo propriete in typeof(TEntite).GetProperties())
{
if (propriete.Name == "Id") continue; // Ignorer la clé primaire pour la mise à jour
object valeur = propriete.GetValue(entite);
string valeurFormatee = propriete.PropertyType == typeof(string) ? $"'{valeur}'" : valeur.ToString();
colonnesValeurs.Add($"{propriete.Name} = {valeurFormatee}");
}
string sql = $"UPDATE {nomTable} SET {string.Join(", ", colonnesValeurs)} WHERE Id = {entite.Id}";
return ExecuterRequeteModification(sql);
}
public bool CreerEnregistrement(TEntite entite)
{
string nomTable = typeof(TEntite).Name;
var colonnes = new List<string>();
var valeurs = new List<string>();
foreach (PropertyInfo propriete in typeof(TEntite).GetProperties())
{
object valeur = propriete.GetValue(entite);
colonnes.Add(propriete.Name);
string valeurFormatee = propriete.PropertyType == typeof(string) ? $"'{valeur}'" : valeur.ToString();
valeurs.Add(valeurFormatee);
}
string sql = $"INSERT INTO {nomTable} ({string.Join(", ", colonnes)}) VALUES ({string.Join(", ", valeurs)})";
return ExecuterRequeteModification(sql);
}
public bool Supprimer(int id)
{
string nomTable = typeof(TEntite).Name;
// Note: Une approche plus robuste utiliserait des paramètres pour éviter l'injection SQL
string sql = $"DELETE FROM {nomTable} WHERE Id = {id}";
return ExecuterRequeteModification(sql);
}
public bool CreerTable()
{
string nomTable = typeof(TEntite).Name;
var definitionsColonnes = new List<string>();
// Assurer que la clé primaire 'Id' est présente et définie
definitionsColonnes.Add("Id INT PRIMARY KEY AUTO_INCREMENT");
foreach (PropertyInfo propriete in typeof(TEntite).GetProperties())
{
if (propriete.Name == "Id") continue; // Déjà géré
string typeSql = ConvertirTypeProprieteVersSql(propriete);
definitionsColonnes.Add($"{propriete.Name} {typeSql}");
}
string sql = $"CREATE TABLE IF NOT EXISTS {nomTable} ({string.Join(", ", definitionsColonnes)})";
return ExecuterRequeteModification(sql);
}
}
Les défis majeurs résident dans la manipulation des valeurs des propriétés des instances génériques et la modification de ces valeurs, où property.SetValue() et property.GetValue() sont essentiels pour accéder aux membres dynamiquement.
Conclusion et Améliorations Futures
Pour une application réelle, la chaîne de connexion devrait être externalisée dans un fichier de configuration (par exemple, App.config ou appsettings.json). Ce démonstration constitue une introduction simple. Un framework ORM complet nécessiterait une gestion plus rigoureuse des performances, la prise en charge des clés primaires composites, des types nullable, l'exécution asynchrone, la gestion des transactions, et bien plus encore.