Même si la notion d'attribut fournie par le Framework .NET est assez limitée, il est quand même possible de réaliser des choses assez évoluées quand on la couple avec la réflexion.
Dans cet article, mon objectif sera donc double :
· Présenter la notion d'attribut
· Fournir une assembly de base permettant de gérer l'accès aux données de manière transversale (pour une base SQL Server), simplement en marquant les classes avec un certain nombre d'attributs personnalisés
Cet article est le premier d'une série de 3 :
1. Création de l'assembly de base pour l'accès aux données (cet article),
2. Optimisation du code produit, correction d'anomalies de la première étape, ajout de nouvelles fonctionnalités (lazy loading sur les propriétés complexes, etc.),
3. Création d'une application permettant à partir d'une base de données SQL Server 2000 de générer les classes gestion de base sur les tables choisies, de générer les collections et les éléments unitaires et enfin de générer les procédures stockées correspondantes.
Bien entendu, mon objectif n'est pas de fournir l'application avec un grand « A » qui remplacera tous les développeurs chevronnés d'une SSII, ni de fournir une application industrialisable telle quelle, et encore moins de concurrencer d'excellents produits tels que Code Smith. Non, cet article se situe légèrement en deçà, à savoir démontrer l'utilisation des technologies et des concepts.
2 Un peu de théorie
Un attribut personnalisé est une classe un peu particulière qui hérite de « System.Attribute ». Une fois associée à une assembly, une classe, une propriété ou une méthode, cette nouvelle métadonnée vient enrichir celles existantes. On voit bien ici une des premières limitations : on se contente d'enrichir par des descriptions, et seulement des descriptions, les éléments constitutifs d'une application .NET.
Par la suite, il est possible de récupérer ces métadonnées par réflexion pour réaliser les traitements adéquats.
De manière succincte, la réflexion permet de récupérer la définition d'une assembly, de connaître l'ensemble des types définis (publics ou non) et, pour un type donné, de connaître ses membres (propriétés, méthodes, etc.). Grâce à la réflexion, il devient même possible d'exécuter dynamiquement du code juste à partir du nom d'une méthode d'une classe. Bref, que du bonheur pour le développeur !
Un certain nombre d'attributs sont d'ores et déjà fournis par le Framework. Par exemple :
· Sur une assembly :
o AssemblyVersion : permet de spécifier le numéro de version de l'assembly qui sera générée à la compilation.
· Sur une classe :
o Serializable : permet de spécifier que la classe peut être sérialisée.
· Sur un propriété :
o Bindable : permet de spécifier si la propriété est utilisable en liaison de données ou pas.
· Sur une méthode :
o WebMethod : dans le cadre d'un Web Service, cet attribut permet de rendre la méthode appelable sur HTTP.
Les attributs peuvent aussi être appliqués à d'autres éléments unitaires, tels que décrits dans l'énumération « AttributeTargets » (Modules, Structures, énumérations, constructeurs, champs, événements, interfaces, paramètres, valeurs retournées et délégués).
Voici un exemple en C# d'utilisation de l'attribut « WebMethod ». Vous noterez que l'on utilise un constructeur paramétré afin d'ajouter une description de la WebMethod :
public class Service1 : System.Web.Services.WebService
{
public Service1()
{
//CODEGEN: This call is required by the ASP.NET Web Services Designer
InitializeComponent();
}
#region Component Designer generated code
...
#endregion
[WebMethod(Description="Ceci est le descriptif de la Web Method HelloWorld")]
public string HelloWorld()
{
return "Hello World";
}
}
Un attribut personnalisé est toujours associé à un mode d'application. Il s'agit d'appliquer l'attribut « AttributeUsage » sur notre nouvelle classe afin de :
· déterminer sur quels éléments (Assembly, Classe, Propriété, Méthode ou autre) notre nouvel attribut personnalisé peut s'appliquer,
· savoir combien de fois il peut être appliqué sur le même élément,
· savoir si l'attribut appliqué doit aussi l'être sur les classes héritées.
Voici un exemple en C# d'un attribut personnalisé applicable une seule fois sur une propriété. Vous noterez la présence d'un constructeur personnalisé permettant ainsi d'enrichir avec des paramètres l'application d'un attribut. Dans cet exemple, je me contente d'associer un nom de champ en BD à une propriété d'une classe.
/// <summary>
/// Cration d'un attribut personnalis appliquer sur une proprit et servant de liaison
/// entre une proprit d'une classe et un champ de BD
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DatabaseColAttribute : System.Attribute
{
/// <summary>
/// Constructeur
/// </summary>
/// <param name="columnName"></param>
public DatabaseColAttribute (string columnName, Boolean isIdentityColumn, Boolean isPrimaryKey)
{
...
}
/// <summary>
/// Constructeur rserv aux colonnes de type complexe
/// </summary>
/// <param name="fatherID"></param>
/// <param name="fillTheProperty"></param>
public DatabaseColAttribute (Type type)
{
...
}
/// <summary>
/// Boolen prcisant si la proprit est de type complexe ou pas (= classe)
/// </summary>
private Boolean _isComplexType;
public Boolean IsComplexType
{
...
}
...
}
Vous noterez que les règles de bienséance veulent qu'un attribut personnalisé soit suffixé par « Attribute » au niveau du nom de sa classe. Par contre, au moment de l'utilisation, on omet ce suffixe. Voici un exemple d'utilisation de l'attribut défini précédemment :
/// <summary>
/// Type des lments unitaires de la collection prcdente
/// </summary>
[Serializable()]
public class RegionInfo
{
...
/// <summary>
/// Champ ID de la BD
/// </summary>
private Int32 _RegionId;
[Bindable(true), Sample.Attribute.DatabaseCol("RegionId", false, true)]
public Int32 RegionId
{
get
{
return _RegionId;
}
set
{
_RegionId = value;
}
}
...
/// <summary>
/// Collection des employs associs la rgion
/// </summary>
private EmployeeInfoCollection _employees;
[Bindable(true), Sample.Attribute.DatabaseCol(typeof(WindowsApplication1.Employee))]
public EmployeeInfoCollection Employees
{
get
{
return _employees;
}
set
{
_employees = value;
}
}
}
Pour en terminer avec la théorie, les attributs représentent donc un moyen simple pour enrichir les métadonnées de vos assemblies par l'ajout statique de tags, c'est-à-dire d'annotations déclaratives. C'est au moment de l'exécution qu'il est possible d'écrire du code spécifique pour ajouter des traitements en fonction des valeurs des attributs personnalisés récupérés par réflexion.
3 L'exemple par le code
Certaines tâches sont relativement répétitives dans le développement d'une application. En effet, il nous faut souvent réaliser des opérations de type Select/Insert/Update/Delete sur les tables de notre base de données.
Imaginez maintenant la possibilité de créer des classes qui permettent au minimum de réaliser des actions Insert, Update, Delete et Select sur les tables de notre base de données. Mon objectif est de créer les classes liées aux tables de ma base, de les faire hériter d'une classe de base pour réaliser les traitements et de les marquer à l'aide d'attributs personnalisés afin que la classe de base puisse réaliser les traitements adéquats.
Bien entendu, idéalement, il ne faut pas être fortement lié à la couche technique d'accès aux données. En d'autres termes, il serait souhaitable de récupérer des collections spécialisées lors des différentes sélections. Enfin, dernier point, je souhaiterai pouvoir utiliser la notion de procédure stockée au sein du SGBD ciblé (uniquement SQL Server 2000 pour ma part).
Vous me direz, pourquoi passer par des procédures stockées et pas par un simple marquage de la classe avec le nom de la table permettant ainsi de générer des ordres SQL génériques en fonction du SGBD ciblé ? Simplement pour rester maître des ordres SQL. C'est un choix très structurant, j'en conviens.
3.2 L'architecture retenue
L'assembly « Sample.Attribute » permettra de regrouper les fonctionnalités de base et implémentera les classes suivantes :
· DatabaseSPAttribute : attribut personnalisé à appliquer sur une classe d'accès aux données et permettant de spécifier :
o Le nom de la procédure stockée à exécuter pour la sélection
o Le nom de la procédure stockée à exécuter pour la suppression
o Le nom de la procédure stockée à exécuter pour la mise à jour
o Le nom de la procédure stockée à exécuter pour l'insertion
o Le type de collection à renvoyer pour la sélection
o Le type unitaire des éléments de la collection précédente
· DatabaseColAttribute : attribut personnalisé à appliquer sur une propriété d'une classe et permettant de spécifier :
o Le nom du champ de la table de la base de données à mapper
o Un booléen permettant de spécifier s'il s'agit d'une propriété de type identité (= générée par la base de données)
o Un booléen permettant de spécifier s'il s'agit de la clé primaire (donc à ne pas enregistrer ou mettre à jour)
o S'il s'agit d'un type complexe, un constructeur permet de spécifier le type de l'élément à construire
· AbstractRequester : classe abstraite regroupant l'ensemble des fonctionnalités d'accès aux données et utilisant l'application block Microsoft « Data Access ». Cette classe permet de réaliser l'ensemble des traitements de type CRUD sur une table. C'est dans cette classe que toute l'intelligence applicative est regroupée. Toutes les classes que nous créerons hériteront de cette dernière.
Voici quelques particularités de la classe « AbstractRequester » :
· Le constructeur de la classe est chargé de récupérer les différentes valeurs des attributs personnalisés qui seront appliqués sur les classes dérivées,
· Utilisation du pattern « Singleton » avec verrouillage à double vérification qui permet de ne pas verrouiller l'accès à chaque fois que la méthode est invoquée. Le pattern singleton permettra de récupérer une seule instance de la classe dérivée à chaque utilisation, moyennant la création d'une méthode publique dans cette dernière pour renvoyer le bon type et avoir l'intellisense lors du développement,
· La méthode « Select » est de loin la méthode la plus complexe puisque c'est elle qui devra constituer dynamiquement la bonne collection à partir des types paramétrés à l'aide des attributs personnalisés,
· Je vous invite à regarder la région « Utilities » qui contient quelques méthodes intéressantes lorsqu'on utilise la Réflexion.
L'exemple implémenté pour mes tests est des plus simples et repose sur la base Northwind fournie en standard avec SQL Server 2000. L'idée étant de récupérer pour une ou plusieurs régions, l'ensemble des employés distincts y travaillant. Voici les tables utilisées :

Comme structure de données de stockage, j'ai privilégié les classes héritant de « CollectionBase ». J'ai ainsi créé deux collections typées (vivement les Generics de C# 2.0 !) : une pour les régions, l'autre pour les employés. Une des propriétés d'une région étant bien entendu une collection d'employés. Voici un exemple d'utilisation de l'assembly Sample.Attribute :
using System;
namespace WindowsApplication1
{
/// <summary>
/// Summary description for Region.
/// </summary>
[Sample.Attribute.DatabaseSP("Data Source=(local);Initial Catalog=Northwind;User Id=sa",
"sInsertRegion",
"sSelectRegion",
"sUpdateRegion",
"sDeleteRegion",
typeof(WindowsApplication1.RegionInfoCollection),
typeof(WindowsApplication1.RegionInfo))]
public class Region : Sample.Attribute.AbstractRequester
{
/// <summary>
/// Rcupration de la bonne instance partir du singleton de la classe de base
/// </summary>
/// <returns></returns>
public static Region GetInstance()
{
return (Region)GetInstance(typeof(Region));
}
/// <summary>
/// Remplacement de la mthode de la classe de base pour spcialisation de la valeur retourne
/// </summary>
/// <param name="values"></param>
/// <returns></returns>
public new RegionInfoCollection Select(params object[] values)
{
return (RegionInfoCollection)base.Select(values);
}
}
}
Si vous avez regardé les sources, vous avez dû noter que la méthode GetInstance (récupération du singleton) de la classe de base « AbstractRequester » avait été déclarée « protected ». En effet, j'ai souhaité faire de la sorte afin que la méthode « GetInstance() » de la classe dérivée puisse renvoyer statiquement le bon type afin d'avoir l'Intellisense, ce qui me parait important en terme de facilité de développement. C'est exactement le même concept avec la méthode « Select » qui renvoie statiquement le type de la bonne collection.
Voici le diagramme de classe général :

Enfin, la dernière partie concerne simplement l'affichage du résultat. J'ai construit une petite application Windows Forms en C# des plus simples. L'interface et l'ergonomie sont des plus pauvres, voire même des plus misérables, mais ce n'était pas ma priorité. Elle permet de tester simplement la sélection/ajout/Modification/Suppression des éléments de type région.
Pour le reste, je vous invite à jeter un coup d'oeil au code de l'application référencé dans l'entête de cet article.
4 Conclusion
Dans cet article, je vous ai montré comment enrichir les attributs fournis par le Framework avec vos propres attributs personnalisés. Je vous ai aussi montré comment les exploiter via la réflexion pour en tirer partie au moment de l'exécution. Bien entendu, il reste encore un certain nombre de choses à réaliser dont voici une liste non exhaustive :
· Signer les assemblies
· Tester plus en avant les composants fournis
· Implémenter les fonctionnalités pour les types binaires
· Améliorer les points qui le seront dans le deuxième article
Bien entendu, le fait d'hériter d'une classe de base peut poser un problème lorsque l'on souhaite faire du Remoting. En effet, les classes accessibles par Remoting doivent Hériter de « MarshalByRefObject ». Plusieurs solutions sont alors envisageables :
· Les classes devant être créées peuvent implémenter une interface d'accès aux données en lieu et place de ce qui était fait dans la classe de base. Il s'agirait juste d'une encapsulation des méthodes d'un nouveau « Requester » non abstrait cette fois et qui se chargerait du travail. Cette solution est un peu pénible dans le sens où cela nécessite du travail au niveau de chaque classe d'accès à générer.
· Rendre la classe « AbstractRequester » non abstraite et passer en paramètre des différentes méthodes CRUD l'instance de l'entité BD accédée (Region, Employee dans mon exemple). Cette solution rend l'utilisation un peu plus compliquée puisqu'il faut d'abord créer l'instance de la bonne classe puis passer par une classe à part pour réaliser l'opération CRUD souhaitée.
Mais le Remoting est une autre histoire, à aborder sûrement dans un prochain article. A ce propos, si vous cherchez des informations intéressantes, je vous invite à consulter le site de M. Ingo Rammer.