Propriétés d'extension avec c# 3.0

Description

Ceci est un code qui permet de simuler des propriétés d'extension grâce à aux méthodes d'extension de C# 3.0.
L'idée est de définir 2 méthodes d'extension, l'une servant d'accesseur set et l'autre d'accesseur get, les valeurs étant prises et stockées dans un Dictionary.
J'ai prévu pour cela deux façons de faire :
- première méthode : la méthode servant d'accesseur set doit avoir un nom commençant par "Set", tandis que celle servant d'accesseur get doit avoir un nom commençant par "Get". Le reste des noms doit être égaux pour les 2 méthodes. Par exemple :
public static string GetProperty(this IA i)
public static void SetProperty(this IA i, string value)

- deuxième méthode : les 2 méthodes portent le même nom mais des attributs "Get" et "Set" leurs sont associées. Par exemple :
[Get]
public static string AProperty(this IA i) { return string.Empty; }
[Set]
public static void AProperty(this IA i, string value) { }

J'intercepte, les appels de ces méthodes afin de centraliser le remplissage/renvoi de valeurs vers/depuis mon Dictionary. J'utilise pour cela PostSharp (http://www.postsharp.org/).
Je profite de cet outil pour vérifier à la compilation que les conditions citées plus haut sont bien remplies, que des attributs ne sont pas oubliés, que le type d'entré (type du paramètre de la méthode set) et de sorti (type de retour de la méthode get) correspondent,...
En combinant ça avec des interfaces, on pourrait presque faire de l'héritage multiple...

Source / Exemple :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;
using System.Reflection;
using PostSharp.Extensibility;
using System.Text.RegularExpressions;

namespace ExtensionProperties
{
    /// <summary>
    /// Classe de base pour intercepter les méthodes d'extension "propriétés"
    /// </summary>
    [Serializable]
    public abstract class BasePropertyInterceptor : OnMethodBoundaryAspect
    {
        protected static PropertyContainer PropertyValues = PropertyContainer.GetInstance();

        public override void OnEntry(MethodExecutionEventArgs e)
        {
            OwnerType = e.Method.GetParameters()[0].ParameterType;
            Owner = e.GetArguments()[0];
            if (e.GetArguments().Length > 1)
                Value = e.GetArguments()[1];
        }

        public override void CompileTimeInitialize(MethodBase method)
        {
            MethodName = method.Name;
        }

        /// <summary>
        /// Type de l'objet portant la méthode-"propriété".
        /// </summary>
        public Type OwnerType { get; set; }
        
        /// <summary>
        /// Instance de l'objet portant la méthode-"propriété".
        /// </summary>        
        public object Owner { get; set; }

        /// <summary>
        /// Nom de la méthode-"propriété".
        /// </summary>
        public virtual string MethodName { get; set; } 
        
        /// <summary>
        /// Valeur de la propriété.
        /// </summary>       
        public object Value { get; set; }

        [Serializable]
        protected class PropertyContainer : Dictionary<Type, Dictionary<object, Dictionary<string, object>>>
        {
            // Design pattern singleton
            private static PropertyContainer instance = null;

            private PropertyContainer() { }

            public static PropertyContainer GetInstance()
            {
                if (instance == null)
                    instance = new PropertyContainer();
                return instance;
            }

            // Redéfinition d'indexeurs
            public object this[Type objtype, object obj, string propertyName]
            {
                get
                {
                    try { return base[objtype][obj][propertyName]; }
                    catch { return null; }
                }
            }

            public Dictionary<string, object> this[Type objtype, object obj]
            {
                get
                {
                    try { return base[objtype][obj]; }
                    catch { return null; }
                }
            }
        }    
    }

    /// <summary>
    /// Attribut à associer à la classe contenant les méthodes d'extension. Il intercepte les méthodes d'extension "Get*"/"Set*" et renvoit/stock les valeurs.
    /// </summary>
    [Serializable]
    public class InterceptProperties : BasePropertyInterceptor
    {
        private Get _Getter;
        private Set _Setter;

        public override void CompileTimeInitialize(MethodBase method)
        {
            base.CompileTimeInitialize(method);
            if (method.Name.StartsWith("Get"))
            {
                CheckAccessorAttributes(method);

                // la méthode "Get" doit être sans paramètre.
                if (method.GetParameters().Length != 1)
                    throw new Exception(string.Format("Une méthode dont le nom commence par \"Get\" a été trouvée, mais elle possède un ou plusieurs paramètres sans modificateur \"this\".", method.Name));
                var instance = method.GetParameters()[0];

                // on cherche une methode portée par la même classe, de même nom et possédant un attribut "Set".
                var presumedSetters = method.DeclaringType.GetMethods().Where(m => m.Name.StartsWith("Set") && m.GetParameters().Length > 0 && m.GetParameters()[0].ParameterType.ToString() == instance.ParameterType.ToString());
                if (presumedSetters.Count() == 0)
                    throw new Exception(string.Format("Imposssible de trouver une méthode \"{0}\" de {1} dont le nom commence par \"Set\".", method.Name, instance.ParameterType));
            }
            if (method.Name.StartsWith("Set"))
            {
                CheckAccessorAttributes(method);

                // on vérifie que la méthode ne possède qu'un seul paramètre.
                ParameterInfo param;
                if (method.GetParameters().Length != 2)
                    throw new Exception(string.Format("La méthode \"{0}\" définie comme accesseur \"set\" doit posséder un unique paramètre sans modificateur \"this\".", method.Name));
                param = method.GetParameters()[1];
                var instance = method.GetParameters()[0];

                // on cherche une methode portée par la même classe, de même nom et possédant un attribut "Get".
                var presumedGetters = method.DeclaringType.GetMethods().Where(m => m.Name.StartsWith("Get") && m.GetParameters().Length > 0 && m.GetParameters()[0].ParameterType.ToString() == instance.ParameterType.ToString());
                if (presumedGetters.Count() == 0)
                    throw new Exception(string.Format("Imposssible de trouver une méthode \"{0}\" de {1} dont le nom commence par \"Get\".", method.Name, instance.ParameterType));

                // la méthode "Get" doit avoir une valeur de retour du même type que le paramètre de la méthode "Set".
                presumedGetters = presumedGetters.Where(m => m.ReturnType == param.ParameterType);
                if (presumedGetters.Count() == 0)
                    throw new Exception(string.Format("Une méthode dont le nom commence par \"Get\" a été trouvée, mais le type de son paramètre de retour ne correspond pas à celui du paramètre de la méthode \"Set\".", method.Name));
            }
        }

        protected void CheckAccessorAttributes(MethodBase method)
        {
            var setAttr = method.GetCustomAttributes(typeof(Set), false);
            var getAttr = method.GetCustomAttributes(typeof(Get), false);
            if (setAttr != null && setAttr.Length != 0)
                throw new Exception(string.Format("La méthode \"{0}\" ne doit pas avoir d'attribut \"Set\" associé",method.Name));
            if (getAttr != null && getAttr.Length != 0)
                throw new Exception(string.Format("La méthode \"{0}\" ne doit pas avoir d'attribut \"Get\" associé",method.Name));
        }

        public override void OnEntry(MethodExecutionEventArgs e)
        {
            base.OnEntry(e);
            if (e.Method.Name.StartsWith("Set"))
                Setter.OnEntry(e);
        }

        public override void OnExit(MethodExecutionEventArgs e)
        {
            if (e.Method.Name.StartsWith("Get"))
                Getter.OnExit(e);
        }

        public override string MethodName
        {
            get { return base.MethodName.Substring(3); }
        }

        // Il faut propager les infos OwnerType, Owner, MethodName et Value vers nos objets Getter et Setter car ceux-ci n'interceptent pas directement les méthodes.
        private Get Getter
        {
            get
            {
                if (_Getter == null || (_Getter!=null && _Getter.Owner!=this.Owner))
                    _Getter = new Get
                    {
                        OwnerType = this.OwnerType,
                        Owner = this.Owner,
                        MethodName = this.MethodName,
                        Value = this.Value
                    };
                return _Getter;
            }
        }

        private Set Setter
        {
            get
            {
                if (_Getter == null || (_Getter!=null && _Getter.Owner!=this.Owner))
                    _Setter = new Set
                    {
                        OwnerType = this.OwnerType,
                        Owner = this.Owner,
                        MethodName = this.MethodName,
                        Value = this.Value
                    };
                return _Setter;
            }
        }
    }

    /// <summary>
    /// Attribut à associer à une méthode d'extension pour que celle-ci remplace un accesseur "get" d'une propriété.
    /// </summary>
    [Serializable]
    [MulticastAttributeUsage(MulticastTargets.Method, PersistMetaData = true)]
    public class Get : BasePropertyInterceptor
    {
        public override void CompileTimeInitialize(MethodBase method)
        {
            base.CompileTimeInitialize(method);
            // la méthode "Get" doit être sans paramètre.
            if (method.GetParameters().Length != 1)
                throw new Exception(string.Format("Une méthode \"{0}\" portant un attribut \"Get\" a été trouvée, mais elle possède un ou plusieurs paramètres sans modificateur \"this\".", method.Name));
            var instance = method.GetParameters()[0];

            // on cherche une methode portée par la même classe, de même nom et possédant un attribut "Set".
            var presumedSetters = method.DeclaringType.GetMethods().Where(m =>
            {
                if (m.Name == method.Name && m.GetParameters().Length > 0 && m.GetParameters()[0].ParameterType.ToString() == instance.ParameterType.ToString())
                {
                    var attrs = m.GetCustomAttributes(typeof(Set), false);
                    if (attrs != null && attrs.Length != 0)
                        return true;
                }
                return false;
            });
            if (presumedSetters.Count() == 0)
                throw new Exception(string.Format("Imposssible de trouver une méthode \"{0}\" de {1} portant un attribut \"Set\".", method.Name, instance.ParameterType));
        }

        public override void OnExit(MethodExecutionEventArgs e)
        {
            e.ReturnValue = PropertyValues[OwnerType, Owner, MethodName];
        }
    }

    /// <summary>
    /// Attribut à associer à une méthode d'extension pour que celle-ci remplace un accesseur "set" d'une propriété.
    /// </summary>
    [Serializable]
    [MulticastAttributeUsage(MulticastTargets.Method, PersistMetaData=true)]
    public class Set : BasePropertyInterceptor
    {
        public override void CompileTimeInitialize(MethodBase method)
        {
            base.CompileTimeInitialize(method);
            // on vérifie que la méthode ne possède qu'un seul paramètre.
            if (method.GetParameters().Length != 2)
                throw new Exception(string.Format("La méthode \"{0}\" définie comme accesseur \"set\" doit posséder un unique paramètre sans modificateur \"this\".", method.Name));
            var param = method.GetParameters()[1];
            var instance = method.GetParameters()[0];

            // on cherche une methode portée par la même classe, de même nom et possédant un attribut "Get".
            var presumedGetters = method.DeclaringType.GetMethods().Where(m => 
                {
                    if (m.Name == method.Name && m.GetParameters().Length > 0 && m.GetParameters()[0].ParameterType.ToString() == instance.ParameterType.ToString())
                    {
                        var attrs = m.GetCustomAttributes(typeof(Get), false);
                        if (attrs != null && attrs.Length != 0)
                            return true;
                    }
                    return false;
                });
            if(presumedGetters.Count()==0)
                throw new Exception(string.Format("Imposssible de trouver une méthode \"{0}\" de {1} portant un attribut \"Get\".", method.Name, instance.ParameterType));

            // la méthode "Get" doit avoir une valeur de retour du même type que le paramètre de la méthode "Set".
            presumedGetters = presumedGetters.Where(m => m.ReturnType == param.ParameterType);
            if (presumedGetters.Count() == 0)
                throw new Exception(string.Format("Une méthode \"{0}\" portant un attribut \"Get\" a été trouvée, mais le type de son paramètre de retour ne correspond pas à celui du paramètre de la méthode \"Set\".", method.Name));
        }

        public override void OnEntry(MethodExecutionEventArgs e)
        {
            base.OnEntry(e);
            if (Value == null)
                return;
            if (!PropertyValues.ContainsKey(OwnerType))
                PropertyValues.Add(OwnerType, new Dictionary<object, Dictionary<string, object>>());
            if (!PropertyValues[OwnerType].ContainsKey(Owner))
                PropertyValues[OwnerType].Add(Owner, new Dictionary<string, object>());
            if (!PropertyValues[OwnerType, Owner].ContainsKey(MethodName))
                PropertyValues[OwnerType, Owner].Add(MethodName, Value);
        }
    }
}

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.