Comment intercepter un appel de méthode en C #?


154

Pour une classe donnée, je voudrais avoir une fonctionnalité de traçage, c'est-à-dire que je voudrais enregistrer chaque appel de méthode (signature de méthode et valeurs réelles des paramètres) et chaque sortie de méthode (juste la signature de méthode).

Comment puis-je accomplir cela en supposant que:

  • Je ne souhaite pas utiliser de bibliothèques AOP tierces pour C #,
  • Je ne veux pas ajouter de code en double à toutes les méthodes que je souhaite tracer,
  • Je ne veux pas changer l'API publique de la classe - les utilisateurs de la classe devraient pouvoir appeler toutes les méthodes exactement de la même manière.

Pour rendre la question plus concrète, supposons qu'il existe 3 classes:

 public class Caller 
 {
     public static void Call() 
     {
         Traced traced = new Traced();
         traced.Method1();
         traced.Method2(); 
     }
 }

 public class Traced 
 {
     public void Method1(String name, Int32 value) { }

     public void Method2(Object object) { }
 }

 public class Logger
 {
     public static void LogStart(MethodInfo method, Object[] parameterValues);

     public static void LogEnd(MethodInfo method);
 }

Comment appeler Logger.LogStart et Logger.LogEnd pour chaque appel à Method1 et Method2 sans modifier la méthode Caller.Call et sans ajouter explicitement les appels à Traced.Method1 et Traced.Method2 ?

Edit: Quelle serait la solution si je suis autorisé à modifier légèrement la méthode Call?



1
Si vous voulez savoir comment fonctionne l'interception en C #, jetez un œil à Tiny Interceptor . Cet exemple s'exécute sans aucune dépendance. Notez que si vous souhaitez utiliser AOP dans des projets réels, n'essayez pas de l'implémenter vous-même. Utilisez des bibliothèques comme PostSharp.
Jalal

J'ai implémenté la journalisation d'un appel de méthode (avant et après) à l'aide de la bibliothèque MethodDecorator.Fody. Veuillez consulter la bibliothèque sur github.com/Fody/MethodDecorator
Dilhan Jayathilake

Réponses:


69

C # n'est pas un langage orienté AOP. Il a quelques fonctionnalités AOP et vous pouvez en émuler d'autres, mais créer AOP avec C # est douloureux.

J'ai recherché des moyens de faire exactement ce que vous vouliez faire et je n'ai trouvé aucun moyen facile de le faire.

Si je comprends bien, voici ce que vous voulez faire:

[Log()]
public void Method1(String name, Int32 value);

et pour ce faire, vous avez deux options principales

  1. Héritez votre classe de MarshalByRefObject ou ContextBoundObject et définissez un attribut qui hérite de IMessageSink. Cet article a un bon exemple. Vous devez néanmoins considérer qu'en utilisant un MarshalByRefObject, les performances seront terribles, et je le pense, je parle d'une performance 10x perdue, alors réfléchissez bien avant d'essayer cela.

  2. L'autre option est d'injecter du code directement. Au moment de l'exécution, cela signifie que vous devrez utiliser la réflexion pour «lire» chaque classe, obtenir ses attributs et injecter l'appel approprié (et d'ailleurs je pense que vous ne pouvez pas utiliser la méthode Reflection.Emit comme je pense que Reflection.Emit ne le ferait pas) ne vous permet pas d'insérer un nouveau code dans une méthode déjà existante). Au moment de la conception, cela signifiera la création d'une extension du compilateur CLR dont je n'ai honnêtement aucune idée de la façon dont cela est fait.

La dernière option utilise un framework IoC . Ce n'est peut-être pas la solution parfaite car la plupart des frameworks IoC fonctionnent en définissant des points d'entrée qui permettent aux méthodes d'être accrochées, mais, en fonction de ce que vous souhaitez obtenir, cela pourrait être une approximation juste.


62
En d'autres termes, 'aïe'
johnc

2
Je dois souligner que si vous aviez des fonctions de première classe, alors une fonction pourrait être traitée comme n'importe quelle autre variable et vous pourriez avoir un "hook de méthode" qui fait ce qu'il veut.
RCIX

3
Une troisième alternative consiste à générer un proxy aop basé sur l'héritage au moment de l'exécution en utilisant Reflection.Emit. C'est l' approche choisie par Spring.NET . Cependant, cela nécessiterait des méthodes virtuelles Tracedet ne convient pas vraiment à une utilisation sans une sorte de conteneur IOC, je comprends donc pourquoi cette option ne figure pas dans votre liste.
Marijn

2
votre deuxième option est essentiellement "Ecrire les parties d'un framework AOP dont vous avez besoin à la main", ce qui devrait alors donner les inferrens de "Oh attendez peut-être que je devrais utiliser une option tierce créée spécifiquement pour résoudre le problème que j'ai au lieu de descendre non -inveted-here-road "
Rune FS

2
@jorge Pouvez-vous fournir un exemple / lien pour y parvenir en utilisant Dependency Injection / IoC famework comme nInject
Charanraj Golla

48

Le moyen le plus simple d'y parvenir est probablement d'utiliser PostSharp . Il injecte du code dans vos méthodes en fonction des attributs que vous lui appliquez. Cela vous permet de faire exactement ce que vous voulez.

Une autre option consiste à utiliser l' API de profilage pour injecter du code dans la méthode, mais c'est vraiment hardcore.


3
vous pouvez également injecter des trucs avec ICorDebug mais c'est super diabolique
Sam Saffron

9

Si vous écrivez une classe - appelez-la Tracing - qui implémente l'interface IDisposable, vous pouvez envelopper tous les corps de méthode dans un

Using( Tracing tracing = new Tracing() ){ ... method body ...}

Dans la classe Tracing, vous pouvez gérer la logique des traces dans la méthode constructeur / Dispose, respectivement, dans la classe Tracing pour garder une trace de l'entrée et de la sortie des méthodes. Tel que:

    public class Traced 
    {
        public void Method1(String name, Int32 value) {
            using(Tracing tracer = new Tracing()) 
            {
                [... method body ...]
            }
        }

        public void Method2(Object object) { 
            using(Tracing tracer = new Tracing())
            {
                [... method body ...]
            }
        }
    }

ressemble à beaucoup d'effort
LeRoi

3
Cela n'a rien à voir avec la réponse à la question.
Latence du

9

Vous pouvez y parvenir avec la fonction d' interception d'un conteneur DI tel que Castle Windsor . En effet, il est possible de configurer le conteneur de manière à ce que toutes les classes ayant une méthode décorée par un attribut spécifique soient interceptées.

Concernant le point n ° 3, OP a demandé une solution sans cadre AOP. J'ai supposé dans la réponse suivante que ce qui devrait être évité était Aspect, JointPoint, PointCut, etc. Selon la documentation d'interception de CastleWindsor , aucun de ceux-ci n'est requis pour accomplir ce qui est demandé.

Configurez l'enregistrement générique d'un intercepteur, basé sur la présence d'un attribut:

public class RequireInterception : IContributeComponentModelConstruction
{
    public void ProcessModel(IKernel kernel, ComponentModel model)
    {
        if (HasAMethodDecoratedByLoggingAttribute(model.Implementation))
        {
            model.Interceptors.Add(new InterceptorReference(typeof(ConsoleLoggingInterceptor)));
            model.Interceptors.Add(new InterceptorReference(typeof(NLogInterceptor)));
        }
    }

    private bool HasAMethodDecoratedByLoggingAttribute(Type implementation)
    {
        foreach (var memberInfo in implementation.GetMembers())
        {
            var attribute = memberInfo.GetCustomAttributes(typeof(LogAttribute)).FirstOrDefault() as LogAttribute;
            if (attribute != null)
            {
                return true;
            }
        }

        return false;
    }
}

Ajouter le IContributeComponentModelConstruction créé au conteneur

container.Kernel.ComponentModelBuilder.AddContributor(new RequireInterception());

Et tu peux faire ce que tu veux dans l'intercepteur lui-même

public class ConsoleLoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.Writeline("Log before executing");
        invocation.Proceed();
        Console.Writeline("Log after executing");
    }
}

Ajoutez l'attribut de journalisation à votre méthode pour journaliser

 public class Traced 
 {
     [Log]
     public void Method1(String name, Int32 value) { }

     [Log]
     public void Method2(Object object) { }
 }

Notez qu'une certaine manipulation de l'attribut sera nécessaire si seule une méthode d'une classe doit être interceptée. Par défaut, toutes les méthodes publiques seront interceptées.


5

Si vous voulez suivre vos méthodes sans limitation (pas d'adaptation de code, pas de Framework AOP, pas de code en double), laissez-moi vous dire, vous avez besoin d'un peu de magie ...

Sérieusement, je l'ai résolu pour implémenter un Framework AOP fonctionnant au moment de l'exécution.

Vous pouvez trouver ici: NConcern .NET AOP Framework

J'ai décidé de créer ce Framework AOP pour répondre à ce type de besoins. c'est une bibliothèque simple très légère. Vous pouvez voir un exemple de logger sur la page d'accueil.

Si vous ne souhaitez pas utiliser un assembly tiers, vous pouvez parcourir le code source (open source) et copier les deux fichiers Aspect.Directory.cs et Aspect.Directory.Entry.cs pour les adapter selon vos souhaits. Ces classes permettent de remplacer vos méthodes lors de l'exécution. Je vous demanderais simplement de respecter la licence.

J'espère que vous trouverez ce dont vous avez besoin ou pour vous convaincre d'utiliser enfin un Framework AOP.



4

J'ai trouvé un moyen différent qui peut être plus facile ...

Déclarer une méthode InvokeMethod

[WebMethod]
    public object InvokeMethod(string methodName, Dictionary<string, object> methodArguments)
    {
        try
        {
            string lowerMethodName = '_' + methodName.ToLowerInvariant();
            List<object> tempParams = new List<object>();
            foreach (MethodInfo methodInfo in serviceMethods.Where(methodInfo => methodInfo.Name.ToLowerInvariant() == lowerMethodName))
            {
                ParameterInfo[] parameters = methodInfo.GetParameters();
                if (parameters.Length != methodArguments.Count()) continue;
                else foreach (ParameterInfo parameter in parameters)
                    {
                        object argument = null;
                        if (methodArguments.TryGetValue(parameter.Name, out argument))
                        {
                            if (parameter.ParameterType.IsValueType)
                            {
                                System.ComponentModel.TypeConverter tc = System.ComponentModel.TypeDescriptor.GetConverter(parameter.ParameterType);
                                argument = tc.ConvertFrom(argument);

                            }
                            tempParams.Insert(parameter.Position, argument);

                        }
                        else goto ContinueLoop;
                    }

                foreach (object attribute in methodInfo.GetCustomAttributes(true))
                {
                    if (attribute is YourAttributeClass)
                    {
                        RequiresPermissionAttribute attrib = attribute as YourAttributeClass;
                        YourAttributeClass.YourMethod();//Mine throws an ex
                    }
                }

                return methodInfo.Invoke(this, tempParams.ToArray());
            ContinueLoop:
                continue;
            }
            return null;
        }
        catch
        {
            throw;
        }
    }

Je définis ensuite mes méthodes comme ça

[WebMethod]
    public void BroadcastMessage(string Message)
    {
        //MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast: <b>" + Message + "</b></span>");
        //return;
        InvokeMethod("BroadcastMessage", new Dictionary<string, object>() { {"Message", Message} });
    }

    [RequiresPermission("editUser")]
    void _BroadcastMessage(string Message)
    {
        MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast: <b>" + Message + "</b></span>");
        return;
    }

Maintenant, je peux avoir la vérification au moment de l'exécution sans l'injection de dépendance ...

Pas de pièges sur le site :)

J'espère que vous conviendrez qu'il s'agit d'un poids moindre qu'un Framework AOP ou dérivant de MarshalByRefObject ou utilisant des classes à distance ou proxy.


4

Vous devez d'abord modifier votre classe pour implémenter une interface (plutôt que d'implémenter MarshalByRefObject).

interface ITraced {
    void Method1();
    void Method2()
}
class Traced: ITraced { .... }

Ensuite, vous avez besoin d'un objet wrapper générique basé sur RealProxy pour décorer n'importe quelle interface afin de permettre l'interception de tout appel à l'objet décoré.

class MethodLogInterceptor: RealProxy
{
     public MethodLogInterceptor(Type interfaceType, object decorated) 
         : base(interfaceType)
     {
          _decorated = decorated;
     }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase;
        Console.WriteLine("Precall " + methodInfo.Name);
        var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
        Console.WriteLine("Postcall " + methodInfo.Name);

        return new ReturnMessage(result, null, 0,
            methodCall.LogicalCallContext, methodCall);
    }
}

Nous sommes maintenant prêts à intercepter les appels à la méthode 1 et à la méthode 2 de ITraced

 public class Caller 
 {
     public static void Call() 
     {
         ITraced traced = (ITraced)new MethodLogInterceptor(typeof(ITraced), new Traced()).GetTransparentProxy();
         traced.Method1();
         traced.Method2(); 
     }
 }

2

Vous pouvez utiliser le framework open source CInject sur CodePlex. Vous pouvez écrire un code minimal pour créer un injecteur et le faire intercepter rapidement tout code avec CInject. De plus, comme il s'agit d'Open Source, vous pouvez également l'étendre.

Vous pouvez également suivre les étapes mentionnées dans cet article sur les appels de méthode d'interception à l'aide d'IL et créer votre propre intercepteur à l'aide des classes Reflection.Emit en C #.


1

Je ne connais pas de solution mais mon approche serait la suivante.

Décorez la classe (ou ses méthodes) avec un attribut personnalisé. A un autre endroit du programme, laissez une fonction d'initialisation refléter tous les types, lisez les méthodes décorées avec les attributs et injectez du code IL dans la méthode. Il pourrait en fait être plus pratique de remplacer la méthode par un stub qui appelle LogStart, la méthode réelle, puis LogEnd. De plus, je ne sais pas si vous pouvez modifier les méthodes en utilisant la réflexion, il pourrait donc être plus pratique de remplacer le type entier.


1

Vous pouvez potentiellement utiliser le modèle GOF Decorator et «décorer» toutes les classes qui nécessitent un traçage.

Ce n'est probablement vraiment pratique qu'avec un conteneur IOC (mais comme pointeur plus tôt, vous voudrez peut-être envisager une interception de méthode si vous allez suivre le chemin IOC).



1

AOP est un must pour l'implémentation de code propre, mais si vous souhaitez entourer un bloc en C #, les méthodes génériques ont une utilisation relativement plus facile. (avec un sens intelli et un code fortement typé) Certes, cela ne peut PAS être une alternative à AOP.

Bien que PostSHarp ait peu de problèmes de buggy (je ne me sens pas confiant pour l'utilisation en production), c'est une bonne chose.

Classe wrapper générique,

public class Wrapper
{
    public static Exception TryCatch(Action actionToWrap, Action<Exception> exceptionHandler = null)
    {
        Exception retval = null;
        try
        {
            actionToWrap();
        }
        catch (Exception exception)
        {
            retval = exception;
            if (exceptionHandler != null)
            {
                exceptionHandler(retval);
            }
        }
        return retval;
    }

    public static Exception LogOnError(Action actionToWrap, string errorMessage = "", Action<Exception> afterExceptionHandled = null)
    {
        return Wrapper.TryCatch(actionToWrap, (e) =>
        {
            if (afterExceptionHandled != null)
            {
                afterExceptionHandled(e);
            }
        });
    }
}

l'utilisation pourrait être comme ça (avec un sens intelli bien sûr)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");

Je suis d'accord que Postsharp est une bibliothèque AOP et gérera l'interception, mais votre exemple n'illustre rien de tel. Ne confondez pas IoC et interception. Ils ne sont pas les mêmes.
Latence du

-1
  1. Écrivez votre propre bibliothèque AOP.
  2. Utilisez la réflexion pour générer un proxy de journalisation sur vos instances (vous ne savez pas si vous pouvez le faire sans modifier une partie de votre code existant).
  3. Réécrivez l'assembly et injectez votre code de journalisation (essentiellement le même que 1).
  4. Hébergez le CLR et ajoutez la journalisation à ce niveau (je pense que c'est la solution la plus difficile à mettre en œuvre, je ne sais pas si vous avez les crochets requis dans le CLR).

-3

Le mieux que vous puissiez faire avant la sortie de C # 6 avec 'nameof' est d'utiliser des expressions lentes StackTrace et linq.

Par exemple pour une telle méthode

    public void MyMethod(int age, string name)
    {
        log.DebugTrace(() => age, () => name);

        //do your stuff
    }

Une telle ligne peut être produite dans votre fichier journal

Method 'MyMethod' parameters age: 20 name: Mike

Voici la mise en œuvre:

    //TODO: replace with 'nameof' in C# 6
    public static void DebugTrace(this ILog log, params Expression<Func<object>>[] args)
    {
        #if DEBUG

        var method = (new StackTrace()).GetFrame(1).GetMethod();

        var parameters = new List<string>();

        foreach(var arg in args)
        {
            MemberExpression memberExpression = null;
            if (arg.Body is MemberExpression)
                memberExpression = (MemberExpression)arg.Body;

            if (arg.Body is UnaryExpression && ((UnaryExpression)arg.Body).Operand is MemberExpression)
                memberExpression = (MemberExpression)((UnaryExpression)arg.Body).Operand;

            parameters.Add(memberExpression == null ? "NA" : memberExpression.Member.Name + ": " + arg.Compile().DynamicInvoke().ToString());
        }

        log.Debug(string.Format("Method '{0}' parameters {1}", method.Name, string.Join(" ", parameters)));

        #endif
    }

Cela échoue à l'exigence définie dans son deuxième point.
Ted Bigham
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.