Come posso intercettare una chiamata di metodo in C #?


154

Per una data classe vorrei avere la funzionalità di traccia, cioè vorrei registrare ogni chiamata di metodo (firma del metodo e valori dei parametri effettivi) e ogni uscita del metodo (solo la firma del metodo).

Come posso ottenere questo presupponendo che:

  • Non voglio usare librerie AOP di terze parti per C #,
  • Non voglio aggiungere un codice duplicato a tutti i metodi che voglio tracciare,
  • Non voglio cambiare l'API pubblica della classe: gli utenti della classe dovrebbero essere in grado di chiamare tutti i metodi esattamente allo stesso modo.

Per rendere la domanda più concreta, supponiamo che ci siano 3 classi:

 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);
 }

Come posso invocare Logger.LogStart e Logger.LogEnd per ogni chiamata a Method1 e Method2 senza modificare il metodo Caller.Call e senza aggiungere esplicitamente le chiamate a Traced.Method1 e Traced.Method2 ?

Modifica: quale sarebbe la soluzione se potessi modificare leggermente il metodo di chiamata?



1
Se vuoi sapere come funziona l'intercettazione in C # dai un'occhiata a Tiny Interceptor . Questo esempio viene eseguito senza dipendenze. Si noti che se si desidera utilizzare AOP nei progetti del mondo reale, non tentare di implementarlo da soli. Usa librerie come PostSharp.
Jalal,

Ho implementato la registrazione di una chiamata di metodo (prima e dopo) utilizzando la libreria MethodDecorator.Fody.
Dai

Risposte:


69

C # non è un linguaggio orientato all'AOP. Ha alcune funzionalità AOP e puoi emularne altre, ma rendere AOP con C # è doloroso.

Ho cercato i modi per fare esattamente quello che volevi fare e non ho trovato un modo semplice per farlo.

A quanto ho capito, questo è quello che vuoi fare:

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

e per farlo hai due opzioni principali

  1. Eredita la tua classe da MarshalByRefObject o ContextBoundObject e definisci un attributo che eredita da IMessageSink. Questo articolo ha un buon esempio. Devi considerare tuttavia che usando un MarshalByRefObject le prestazioni diminuiranno come l'inferno, e intendo dire, sto parlando di una prestazione 10x persa, quindi pensa attentamente prima di provarlo.

  2. L'altra opzione è quella di iniettare il codice direttamente. In runtime, ciò significa che dovrai usare la riflessione per "leggere" ogni classe, ottenere i suoi attributi e iniettare la chiamata appropriata (e del resto penso che tu non possa usare il metodo Reflection.Emit come penso Reflection.Emit vorrebbe non ti permette di inserire un nuovo codice all'interno di un metodo già esistente). In fase di progettazione questo significherà creare un'estensione per il compilatore CLR di cui non ho sinceramente idea su come sia stato fatto.

L'ultima opzione sta usando un framework IoC . Forse non è la soluzione perfetta poiché la maggior parte dei framework IoC funziona definendo i punti di ingresso che consentono ai metodi di essere agganciati ma, a seconda di ciò che si desidera raggiungere, potrebbe essere una corretta approssimazione.


62
In altre parole, 'ouch'
johnc

2
Dovrei sottolineare che se avessi funzioni di prima classe, allora una funzione potrebbe essere trattata come qualsiasi altra variabile e potresti avere un "hook di metodo" che fa quello che vuole.
RCIX,

3
Una terza alternativa è quella di generare un proxy basato su eredità aop in fase di esecuzione utilizzando Reflection.Emit. Questo è l' approccio scelto da Spring.NET . Tuttavia, ciò richiederebbe metodi virtuali Tracede non è davvero adatto all'uso senza una sorta di contenitore IOC, quindi capisco perché questa opzione non è nell'elenco.
Marijn,

2
la tua seconda opzione è fondamentalmente "Scrivi a mano le parti di un framework AOP di cui hai bisogno" che dovrebbe quindi dare per scontato "Oh aspetta forse dovrei usare un'opzione di terze parti creata appositamente per risolvere il problema che ho invece di non scendere -inveted-here-road "
Rune FS

2
@jorge Puoi fornire qualche esempio / link per raggiungere questo obiettivo usando l'iniezione di dipendenza / fama IoC come nInject
Charanraj Golla,

48

Il modo più semplice per raggiungere questo obiettivo è probabilmente utilizzare PostSharp . Inietta il codice all'interno dei tuoi metodi in base agli attributi che ti vengono applicati. Ti permette di fare esattamente quello che vuoi.

Un'altra opzione è quella di utilizzare l' API di profilazione per iniettare codice all'interno del metodo, ma è davvero hardcore.


3
puoi anche iniettare roba con ICorDebug ma è super male
Sam Saffron,

9

Se scrivi una classe - chiamala Traccia - che implementa l'interfaccia IDisposable, potresti avvolgere tutti i corpi del metodo in un

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

Nella classe Tracing è possibile gestire la logica delle tracce nel metodo di costruzione / Dispose, rispettivamente, nella classe Tracing per tenere traccia dell'entrata e dell'uscita dei metodi. Tale che:

    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 ...]
            }
        }
    }

sembra un grande sforzo
LeRoi

3
Questo non ha nulla a che fare con la risposta alla domanda.
Latenza

9

Potresti ottenerlo con la funzione di intercettazione di un contenitore DI come Castle Windsor . In effetti, è possibile configurare il contenitore in modo tale da intercettare ogni classe che ha un metodo decorato da un attributo specifico.

Per quanto riguarda il punto 3, OP ha chiesto una soluzione senza framework AOP. Nella seguente risposta ho assunto che ciò che dovrebbe essere evitato erano Aspect, JointPoint, PointCut, ecc. Secondo la documentazione di Intercettazione di CastleWindsor , nessuno di questi è tenuto a soddisfare ciò che viene chiesto.

Configurare la registrazione generica di un intercettore, in base alla presenza di un attributo:

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;
    }
}

Aggiungi l'IContributeComponentModelConstruction creato al contenitore

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

E puoi fare quello che vuoi nell'intercettore stesso

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

Aggiungi l'attributo di registrazione al tuo metodo per accedere

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

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

Si noti che sarà necessaria una gestione dell'attributo se è necessario intercettare solo un metodo di una classe. Per impostazione predefinita, tutti i metodi pubblici verranno intercettati.


5

Se vuoi tracciare i tuoi metodi senza limitazioni (nessun adattamento del codice, nessun framework AOP, nessun codice duplicato), lascia che te lo dica, hai bisogno di un po 'di magia ...

Seriamente, l'ho risolto per implementare un framework AOP funzionante in fase di esecuzione.

È possibile trovare qui: NConcern .NET AOP Framework

Ho deciso di creare questo AOP Framework per rispondere a questo tipo di esigenze. è una libreria semplice molto leggera. Puoi vedere un esempio di logger nella home page.

Se non si desidera utilizzare un assembly di terze parti, è possibile sfogliare il codice sorgente (open source) e copiare entrambi i file Aspect.Directory.cs e Aspect.Directory.Entry.cs per adattarli come desiderato. Queste classi consentono di sostituire i metodi in fase di esecuzione. Vorrei solo chiederti di rispettare la licenza.

Spero che troverai ciò di cui hai bisogno o per convincerti a utilizzare finalmente un framework AOP.



4

Ho trovato un modo diverso che potrebbe essere più semplice ...

Dichiarare un metodo 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;
        }
    }

Quindi definisco i miei metodi in questo modo

[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;
    }

Ora posso avere il controllo in fase di esecuzione senza l'iniezione di dipendenza ...

Nessun gotchas nel sito :)

Spero che accetti che questo sia meno pesante di un AOP Framework o derivante da MarshalByRefObject o che usi le classi remote o proxy.


4

Per prima cosa devi modificare la tua classe per implementare un'interfaccia (piuttosto che implementare MarshalByRefObject).

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

Successivamente è necessario un oggetto wrapper generico basato su RealProxy per decorare qualsiasi interfaccia per consentire l'intercettazione di qualsiasi chiamata all'oggetto decorato.

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);
    }
}

Ora siamo pronti per intercettare le chiamate a Method1 e Method2 di ITraced

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

2

È possibile utilizzare il framework open source CInject su CodePlex. È possibile scrivere un codice minimo per creare un iniettore e farlo intercettare rapidamente qualsiasi codice con CInject. Inoltre, poiché si tratta di Open Source, è possibile estendere anche questo.

In alternativa, puoi seguire i passaggi indicati in questo articolo su Intercettazione di chiamate di metodo utilizzando IL e creare il tuo intercettore utilizzando Reflection.Emit delle classi in C #.


1

Non conosco una soluzione, ma il mio approccio sarebbe il seguente.

Decorare la classe (oi suoi metodi) con un attributo personalizzato. Da qualche altra parte nel programma, lasciare che una funzione di inizializzazione rifletta tutti i tipi, leggere i metodi decorati con gli attributi e iniettare del codice IL nel metodo. In realtà potrebbe essere più pratico sostituire il metodo con uno stub che chiama LogStart, il metodo effettivo e quindi LogEnd. Inoltre, non so se è possibile modificare i metodi utilizzando la riflessione, quindi potrebbe essere più pratico sostituire l'intero tipo.


1

Potresti potenzialmente utilizzare il modello Decoratore GOF e "decorare" tutte le classi che necessitano di tracciamento.

Probabilmente è davvero pratico solo con un contenitore IOC (ma come puntatore in precedenza potresti prendere in considerazione l'intercettazione del metodo se stai andando lungo il percorso IOC).



1

AOP è un must per l'implementazione di codice pulito, tuttavia se si desidera racchiudere un blocco in C #, i metodi generici hanno un utilizzo relativamente più semplice. (con intelli sense e codice fortemente tipizzato) Certamente, NON può essere un'alternativa per AOP.

Sebbene PostSHarp abbia dei piccoli problemi (non mi sento sicuro di usarlo durante la produzione), è una buona cosa.

Classe wrapper generica,

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'utilizzo potrebbe essere così (con intelli ovviamente)

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

Sono d'accordo che Postsharp è una libreria AOP e gestirà l'intercettazione, tuttavia il tuo esempio non illustra nulla del genere. Non confondere l'IoC con l'intercettazione. Non sono gli stessi.
Latenza

-1
  1. Scrivi la tua libreria AOP.
  2. Usa la riflessione per generare un proxy di registrazione sulle tue istanze (non sei sicuro di poterlo fare senza modificare una parte del codice esistente).
  3. Riscrivi l'assembly e inserisci il tuo codice di registrazione (sostanzialmente uguale a 1).
  4. Ospita il CLR e aggiungi la registrazione a questo livello (penso che questa sia la soluzione più difficile da implementare, non sono sicuro che tu abbia gli hook necessari nel CLR).

-3

Il meglio che puoi fare prima che C # 6 con "nameof" sia usare le espressioni StackTrace e linq lente.

Ad esempio per tale metodo

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

        //do your stuff
    }

Tale riga può essere prodotta nel file di registro

Method 'MyMethod' parameters age: 20 name: Mike

Ecco l'implementazione:

    //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
    }

Ciò non soddisfa il requisito definito nel suo secondo punto elenco.
Ted Bigham,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.