Wie fange ich einen Methodenaufruf in C # ab?


154

Für eine bestimmte Klasse möchte ich eine Ablaufverfolgungsfunktion haben, dh ich möchte jeden Methodenaufruf (Methodensignatur und tatsächliche Parameterwerte) und jeden Methodenexit (nur die Methodensignatur) protokollieren.

Wie erreiche ich dies unter der Annahme, dass:

  • Ich möchte keine AOP-Bibliotheken von Drittanbietern für C # verwenden.
  • Ich möchte nicht allen Methoden, die ich verfolgen möchte, doppelten Code hinzufügen.
  • Ich möchte die öffentliche API der Klasse nicht ändern - Benutzer der Klasse sollten in der Lage sein, alle Methoden auf genau dieselbe Weise aufzurufen.

Um die Frage konkreter zu machen, nehmen wir an, dass es drei Klassen gibt:

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

Wie rufe ich Logger.LogStart und Logger.LogEnd für jeden Aufruf von Methode1 und Methode2 auf, ohne die Caller.Call- Methode zu ändern und ohne die Aufrufe explizit zu Traced.Method1 und Traced.Method2 hinzuzufügen ?

Bearbeiten: Was wäre die Lösung, wenn ich die Call-Methode leicht ändern darf?



1
Wenn Sie wissen möchten, wie das Abfangen in C # funktioniert, schauen Sie sich Tiny Interceptor an . Dieses Beispiel wird ohne Abhängigkeiten ausgeführt. Beachten Sie, dass Sie AOP nicht selbst implementieren möchten, wenn Sie es in realen Projekten verwenden möchten. Verwenden Sie Bibliotheken wie PostSharp.
Jalal

Ich habe die Protokollierung eines Methodenaufrufs (vorher und nachher) mithilfe der MethodDecorator.Fody-Bibliothek implementiert. Bitte werfen Sie einen Blick auf die Bibliothek unter github.com/Fody/MethodDecorator
Dilhan Jayathilake

Antworten:


69

C # ist keine AOP-orientierte Sprache. Es hat einige AOP-Funktionen und Sie können einige andere emulieren, aber AOP mit C # zu erstellen ist schmerzhaft.

Ich habe nach Wegen gesucht, genau das zu tun, was Sie wollten, und ich habe keinen einfachen Weg gefunden, dies zu tun.

So wie ich es verstehe, möchten Sie Folgendes tun:

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

und dazu haben Sie zwei Hauptoptionen

  1. Erben Sie Ihre Klasse von MarshalByRefObject oder ContextBoundObject und definieren Sie ein Attribut, das von IMessageSink erbt. Dieser Artikel hat ein gutes Beispiel. Sie müssen jedoch berücksichtigen, dass bei Verwendung eines MarshalByRefObject die Leistung höllisch sinken wird, und ich meine es so, ich spreche von einem 10-fachen Leistungsverlust. Überlegen Sie also sorgfältig, bevor Sie dies versuchen.

  2. Die andere Möglichkeit besteht darin, Code direkt einzufügen. Zur Laufzeit bedeutet dies, dass Sie Reflection verwenden müssen, um jede Klasse zu "lesen", ihre Attribute abzurufen und den entsprechenden Aufruf einzufügen (und ich denke, Sie können die Reflection.Emit-Methode nicht so verwenden, wie Reflection.Emit es meiner Meinung nach nicht tun würde Sie können keinen neuen Code in eine bereits vorhandene Methode einfügen. Zur Entwurfszeit bedeutet dies, eine Erweiterung des CLR-Compilers zu erstellen, von der ich ehrlich gesagt keine Ahnung habe, wie es gemacht wird.

Die letzte Option ist die Verwendung eines IoC-Frameworks . Vielleicht ist es nicht die perfekte Lösung, da die meisten IoC-Frameworks Einstiegspunkte definieren, mit denen Methoden verknüpft werden können. Je nachdem, was Sie erreichen möchten, kann dies jedoch eine faire Annäherung sein.


62
Mit anderen Worten, 'autsch'
Johnc

2
Ich sollte darauf hinweisen, dass wenn Sie erstklassige Funktionen hätten, eine Funktion wie jede andere Variable behandelt werden könnte und Sie einen "Methoden-Hook" haben könnten, der tut, was er will.
RCIX

3
Eine dritte Alternative besteht darin, zur Laufzeit einen vererbungsbasierten aop-Proxy mit zu generieren Reflection.Emit. Dies ist der von Spring.NET gewählte Ansatz . Dies würde jedoch virtuelle Methoden erfordern Tracedund ist für die Verwendung ohne einen IOC-Container nicht wirklich geeignet. Daher verstehe ich, warum diese Option nicht in Ihrer Liste enthalten ist.
Marijn

2
Ihre zweite Option ist im Grunde "Schreiben Sie die Teile eines AOP-Frameworks, die Sie von Hand benötigen", was dann zu den Schlussfolgerungen von "Oh, warte, vielleicht sollte ich eine Option eines Drittanbieters verwenden, die speziell zur Lösung des Problems erstellt wurde, anstatt nicht herunterzufahren." -inveted-here-road "
Rune FS

2
@jorge Können Sie ein Beispiel / einen Link bereitstellen, um dies mit Dependency Injection / IoC-Famework wie nInject
Charanraj Golla

48

Der einfachste Weg, dies zu erreichen, ist wahrscheinlich die Verwendung von PostSharp . Es fügt Code in Ihre Methoden ein, basierend auf den Attributen, die Sie darauf anwenden. Damit können Sie genau das tun, was Sie wollen.

Eine andere Möglichkeit besteht darin, die Profiling-API zu verwenden, um Code in die Methode einzufügen, aber das ist wirklich Hardcore.


3
Sie können auch ICorDebug injizieren, aber das ist super böse
Sam Saffron

9

Wenn Sie eine Klasse schreiben - nennen Sie sie Tracing -, die die IDisposable-Schnittstelle implementiert, können Sie alle Methodenkörper in a einschließen

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

In der Tracing-Klasse können Sie die Logik der Traces in der Konstruktor- / Dispose-Methode bzw. in der Tracing-Klasse behandeln, um den Ein- und Ausgang der Methoden zu verfolgen. So dass:

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

sieht nach viel Mühe aus
LeRoi

3
Dies hat nichts mit der Beantwortung der Frage zu tun.
Latenz

9

Sie können dies mit der Interception- Funktion eines DI-Containers wie Castle Windsor erreichen . In der Tat ist es möglich, den Container so zu konfigurieren, dass alle Klassen, deren Methode durch ein bestimmtes Attribut gekennzeichnet ist, abgefangen werden.

In Bezug auf Punkt 3 bat OP um eine Lösung ohne AOP-Framework. In der folgenden Antwort ging ich davon aus, dass Aspect, JointPoint, PointCut usw. vermieden werden sollten. Laut Interception-Dokumentation von CastleWindsor ist keines davon erforderlich, um das zu erreichen, was gefragt wird.

Konfigurieren Sie die generische Registrierung eines Interceptors basierend auf dem Vorhandensein eines Attributs:

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

Fügen Sie die erstellte IContributeComponentModelConstruction zum Container hinzu

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

Und Sie können im Abfangjäger selbst tun, was Sie wollen

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

Fügen Sie das Protokollierungsattribut Ihrer zu protokollierenden Methode hinzu

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

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

Beachten Sie, dass eine gewisse Behandlung des Attributs erforderlich ist, wenn nur eine Methode einer Klasse abgefangen werden muss. Standardmäßig werden alle öffentlichen Methoden abgefangen.


5

Wenn Sie Ihre Methoden ohne Einschränkung verfolgen möchten (keine Code-Anpassung, kein AOP-Framework, kein doppelter Code), lassen Sie mich Ihnen sagen, Sie brauchen etwas Magie ...

Im Ernst, ich habe es beschlossen, ein AOP-Framework zu implementieren, das zur Laufzeit funktioniert.

Sie finden hier: NConcern .NET AOP Framework

Ich habe beschlossen, dieses AOP-Framework zu erstellen, um auf diese Art von Anforderungen zu reagieren. Es ist eine einfache Bibliothek, die sehr leicht ist. Sie können ein Beispiel für einen Logger auf der Startseite sehen.

Wenn Sie keine Assembly eines Drittanbieters verwenden möchten, können Sie die Codequelle (Open Source) durchsuchen und beide Dateien Aspect.Directory.cs und Aspect.Directory.Entry.cs nach Ihren Wünschen anpassen. Mit diesen Klassen können Sie Ihre Methoden zur Laufzeit ersetzen. Ich möchte Sie nur bitten, die Lizenz zu respektieren.

Ich hoffe, Sie finden, was Sie brauchen, oder überzeugen Sie, endlich ein AOP-Framework zu verwenden.



4

Ich habe einen anderen Weg gefunden, der vielleicht einfacher ist ...

Deklarieren Sie eine Methode 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;
        }
    }

Ich definiere dann meine Methoden so

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

Jetzt kann ich die Prüfung zur Laufzeit ohne die Abhängigkeitsinjektion durchführen lassen ...

Keine Fallstricke vor Ort :)

Hoffentlich stimmen Sie zu, dass dies weniger Gewicht hat als ein AOP-Framework oder von MarshalByRefObject abgeleitet ist oder Remoting- oder Proxy-Klassen verwendet.


4

Zuerst müssen Sie Ihre Klasse ändern, um eine Schnittstelle zu implementieren (anstatt das MarshalByRefObject zu implementieren).

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

Als nächstes benötigen Sie ein generisches Wrapper-Objekt, das auf RealProxy basiert, um eine beliebige Schnittstelle zu dekorieren und das Abfangen eines Aufrufs des dekorierten Objekts zu ermöglichen.

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

Jetzt können wir Aufrufe von Methode1 und Methode2 von ITraced abfangen

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

2

Sie können Open Source Framework CInject auf CodePlex verwenden. Sie können minimalen Code schreiben, um einen Injector zu erstellen und ihn dazu zu bringen, jeden Code mit CInject schnell abzufangen. Da dies Open Source ist, können Sie dies auch erweitern.

Sie können auch die in diesem Artikel beschriebenen Schritte zum Abfangen von Methodenaufrufen mit IL ausführen und mithilfe von Reflection.Emit-Klassen in C # einen eigenen Interceptor erstellen.


1

Ich kenne keine Lösung, aber mein Ansatz wäre wie folgt.

Dekorieren Sie die Klasse (oder ihre Methoden) mit einem benutzerdefinierten Attribut. Lassen Sie an einer anderen Stelle im Programm eine Initialisierungsfunktion alle Typen widerspiegeln, lesen Sie die mit den Attributen dekorierten Methoden und fügen Sie IL-Code in die Methode ein. Es könnte tatsächlich praktischer sein , die Methode durch einen Stub zu ersetzenLogStart , der die eigentliche Methode aufruft und dann LogEnd. Außerdem weiß ich nicht, ob Sie Methoden mithilfe von Reflektion ändern können, sodass es möglicherweise praktischer ist, den gesamten Typ zu ersetzen.


1

Sie können möglicherweise das GOF-Dekorationsmuster verwenden und alle Klassen, die nachverfolgt werden müssen, "dekorieren".

Es ist wahrscheinlich nur mit einem IOC-Container wirklich praktisch (aber als Hinweis darauf sollten Sie das Abfangen von Methoden in Betracht ziehen, wenn Sie den IOC-Pfad entlang gehen).



1

AOP ist ein Muss für die Implementierung von sauberem Code. Wenn Sie jedoch einen Block in C # umgeben möchten, sind generische Methoden relativ einfach zu verwenden. (mit intelligentem Sinn und stark typisiertem Code) Natürlich kann es KEINE Alternative für AOP sein.

Obwohl PostSHarp kleine Buggy-Probleme hat (ich bin nicht sicher, ob ich es in der Produktion verwenden soll), ist es ein gutes Zeug.

Generische Wrapper-Klasse,

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

Die Verwendung könnte so sein (natürlich mit intelligentem Sinn)

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

Ich bin damit einverstanden, dass Postsharp eine AOP-Bibliothek ist und das Abfangen übernimmt. Ihr Beispiel zeigt jedoch nichts dergleichen. Verwechseln Sie IoC nicht mit Abfangen. Sie sind nicht gleich.
Latenz

-1
  1. Schreiben Sie Ihre eigene AOP-Bibliothek.
  2. Verwenden Sie Reflection, um einen Protokollierungs-Proxy für Ihre Instanzen zu generieren (nicht sicher, ob Sie dies tun können, ohne einen Teil Ihres vorhandenen Codes zu ändern).
  3. Schreiben Sie die Assembly neu und fügen Sie Ihren Protokollierungscode ein (im Grunde dasselbe wie 1).
  4. Hosten Sie die CLR und fügen Sie die Protokollierung auf dieser Ebene hinzu (ich denke, dies ist die am schwierigsten zu implementierende Lösung, nicht sicher, ob Sie die erforderlichen Hooks in der CLR haben).

-3

Das Beste, was Sie vor C # 6 mit 'nameof' tun können, ist die Verwendung von langsamen StackTrace- und linq-Ausdrücken.

ZB für eine solche Methode

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

        //do your stuff
    }

Eine solche Zeile wird möglicherweise in Ihrer Protokolldatei erstellt

Method 'MyMethod' parameters age: 20 name: Mike

Hier ist die Implementierung:

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

Dies entspricht nicht der in seinem zweiten Aufzählungspunkt definierten Anforderung.
Ted Bigham
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.