Jak przechwycić wywołanie metody w C #?

154

Dla danej klasy chciałbym mieć funkcjonalność śledzenia tj. Chciałbym logować każde wywołanie metody (podpis metody i rzeczywiste wartości parametrów) i każde wyjście metody (tylko podpis metody).

Jak to osiągnąć, zakładając, że:

  • Nie chcę używać żadnych bibliotek AOP innych firm dla języka C #,
  • Nie chcę dodawać zduplikowanego kodu do wszystkich metod, które chcę śledzić,
  • Nie chcę zmieniać publicznego API klasy - użytkownicy klasy powinni mieć możliwość wywołania wszystkich metod w dokładnie taki sam sposób.

Aby pytanie było bardziej konkretne, przyjmijmy, że istnieją 3 klasy:

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

Jak wywołać Logger.LogStart i Logger.LogEnd dla każdego wywołania Method1 i Method2 bez modyfikowania metody Caller.Call i bez jawnego dodawania wywołań do Traced.Method1 i Traced.Method2 ?

Edycja: Jakie byłoby rozwiązanie, jeśli mogę nieznacznie zmienić metodę Call?

Czeladnik
źródło
1
Jeśli chcesz wiedzieć, jak działa przechwytywanie w C #, zapoznaj się z Tiny Interceptor . Ten przykład działa bez żadnych zależności. Zauważ, że jeśli chcesz używać AOP w rzeczywistych projektach, nie próbuj wdrażać go samodzielnie. Użyj bibliotek, takich jak PostSharp.
Jalal
Zaimplementowałem rejestrowanie wywołania metody (przed i po) przy użyciu biblioteki MethodDecorator.Fody. Zajrzyj do biblioteki pod adresem github.com/Fody/MethodDecorator
Dilhan Jayathilake

Odpowiedzi:

69

C # nie jest językiem zorientowanym na AOP. Ma kilka funkcji AOP i możesz emulować inne, ale tworzenie AOP w C # jest bolesne.

Szukałem sposobów na zrobienie dokładnie tego, co chciałeś, i nie znalazłem łatwego sposobu, aby to zrobić.

Jak rozumiem, oto, co chcesz zrobić:

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

Aby to zrobić, masz dwie główne opcje

  1. Dziedzicz swoją klasę z MarshalByRefObject lub ContextBoundObject i zdefiniuj atrybut, który dziedziczy po IMessageSink. Ten artykuł zawiera dobry przykład. Musisz jednak wziąć pod uwagę, że przy użyciu MarshalByRefObject wydajność spadnie jak diabli, i mam na myśli to, mówię o 10-krotnej utracie wydajności, więc dobrze się zastanów, zanim spróbujesz.

  2. Inną opcją jest bezpośrednie wstrzyknięcie kodu. W środowisku uruchomieniowym, co oznacza, że ​​będziesz musiał użyć odbicia, aby "przeczytać" każdą klasę, pobrać jej atrybuty i wstrzyknąć odpowiednie wywołanie (i myślę, że nie możesz użyć metody Reflection.Emit, tak jak myślę Reflection.Emit nie nie pozwalają na wstawienie nowego kodu do już istniejącej metody). W czasie projektowania będzie to oznaczało stworzenie rozszerzenia do kompilatora CLR, którego szczerze mówiąc nie mam pojęcia, jak to się robi.

Ostatnią opcją jest użycie frameworka IoC . Może nie jest to idealne rozwiązanie, ponieważ większość frameworków IoC działa poprzez definiowanie punktów wejścia, które pozwalają na podpięcie metod, ale w zależności od tego, co chcesz osiągnąć, może to być uczciwe przybliżenie.

Jorge Córdoba
źródło
62
Innymi słowy, „ouch”
johnc
2
Powinienem zwrócić uwagę, że gdybyś miał funkcje pierwszej klasy, to funkcja mogłaby być traktowana jak każda inna zmienna i mógłbyś mieć „punkt zaczepienia metody”, który robi to, czego chce.
RCIX
3
Trzecią alternatywą jest generowanie serwerów proxy aop opartych na dziedziczeniu w czasie wykonywania przy użyciu Reflection.Emit. To jest podejście wybrane przez Spring.NET . Wymagałoby Tracedto jednak włączonych metod wirtualnych i nie nadaje się do użycia bez jakiegoś kontenera IOC, więc rozumiem, dlaczego tej opcji nie ma na Twojej liście.
Marijn,
2
twoją drugą opcją jest po prostu "Napisz ręcznie potrzebne części frameworka AOP", co powinno skutkować wnioskiem "Och, poczekaj może powinienem użyć opcji innej firmy stworzonej specjalnie w celu rozwiązania problemu, który mam zamiast zejść na dół nie -inveted-here-road ”
Rune FS
2
@jorge Czy możesz podać jakiś przykład / link, aby to osiągnąć za pomocą Dependency Injection / IoC, np. nInject
Charanraj Golla
48

Najprostszym sposobem na osiągnięcie tego jest prawdopodobnie użycie PostSharp . Wprowadza kod do twoich metod na podstawie atrybutów, które do niego stosujesz. Pozwala ci robić dokładnie to, co chcesz.

Inną opcją jest użycie interfejsu API profilowania do wstrzyknięcia kodu wewnątrz metody, ale jest to naprawdę trudne.

Antoine Aubry
źródło
3
możesz również wstrzykiwać rzeczy za pomocą ICorDebug, ale to jest super złe
Sam Saffron
9

Jeśli napiszesz klasę - nazwij ją Tracing - która implementuje interfejs IDisposable, możesz opakować wszystkie treści metody w

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

W klasie Tracing można obsługiwać logikę śladów w konstruktorze / metodzie Dispose, odpowiednio w klasie Tracing, aby śledzić wprowadzanie i zamykanie metod. Takie, że:

    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 ...]
            }
        }
    }
Steen
źródło
wygląda na dużo wysiłku
LeRoi
3
Nie ma to nic wspólnego z odpowiedzią na pytanie.
Opóźnienie
9

Możesz to osiągnąć dzięki funkcji przechwytywania kontenera DI, takiego jak Castle Windsor . Rzeczywiście, możliwe jest skonfigurowanie kontenera w taki sposób, że wszystkie klasy, które mają metodę ozdobioną określonym atrybutem, zostałyby przechwycone.

W odniesieniu do punktu 3, OP poprosił o rozwiązanie bez struktury AOP. W poniższej odpowiedzi założyłem, że to, czego należy unikać, to Aspect, JointPoint, PointCut, itp. Zgodnie z dokumentacją przechwytywania z CastleWindsor , żadne z nich nie jest wymagane do wykonania tego, o co proszono.

Skonfiguruj ogólną rejestrację przechwytywacza na podstawie obecności atrybutu:

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

Dodaj utworzoną IContributeComponentModelConstruction do kontenera

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

I możesz robić, co chcesz, w samym przechwytywaczu

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

Dodaj atrybut rejestrowania do metody, aby rejestrować

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

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

Zauważ, że pewna obsługa atrybutu będzie wymagana, jeśli tylko jakaś metoda klasy będzie musiała zostać przechwycona. Domyślnie przechwycone zostaną wszystkie metody publiczne.

plog17
źródło
5

Jeśli chcesz śledzić swoje metody bez ograniczeń (bez adaptacji kodu, bez AOP Framework, bez duplikatu kodu), powiem ci, potrzebujesz trochę magii ...

Poważnie, zdecydowałem się na zaimplementowanie AOP Framework działającego w czasie wykonywania.

Możesz znaleźć tutaj: NConcern .NET AOP Framework

Postanowiłem stworzyć ten AOP Framework, aby odpowiedzieć na tego rodzaju potrzeby. jest to prosta biblioteka, bardzo lekka. Możesz zobaczyć przykład loggera na stronie głównej.

Jeśli nie chcesz używać zespołu innej firmy, możesz przeglądać kod źródłowy (open source) i skopiować oba pliki Aspect.Directory.cs i Aspect.Directory.Entry.cs, aby dostosować je do własnych potrzeb. Te klasy pozwalają na podmianę metod w czasie wykonywania. Chciałbym tylko prosić o przestrzeganie licencji.

Mam nadzieję, że znajdziesz to, czego potrzebujesz lub przekonasz się do ostatecznego wykorzystania AOP Framework.

Tony THONG
źródło
4

Znalazłem inny sposób, który może być łatwiejszy ...

Zadeklaruj metodę 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;
        }
    }

Następnie definiuję swoje metody w ten sposób

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

Teraz mogę sprawdzić w czasie wykonywania bez wstrzykiwania zależności ...

Brak problemów na stronie :)

Miejmy nadzieję, że zgodzisz się, że jest to mniejsza waga niż AOP Framework lub wywodząca się z MarshalByRefObject lub przy użyciu klas zdalnych lub proxy.

Sójka
źródło
4

Najpierw musisz zmodyfikować swoją klasę, aby zaimplementować interfejs (zamiast implementować MarshalByRefObject).

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

Następnie potrzebujesz ogólnego obiektu opakowującego opartego na RealProxy, aby udekorować dowolny interfejs, aby umożliwić przechwycenie dowolnego wywołania dekorowanego obiektu.

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

Teraz jesteśmy gotowi do przechwytywania wywołań metod Method1 i Method2 z ITraced

 public class Caller 
 {
     public static void Call() 
     {
         ITraced traced = (ITraced)new MethodLogInterceptor(typeof(ITraced), new Traced()).GetTransparentProxy();
         traced.Method1();
         traced.Method2(); 
     }
 }
Ibrahim ben Salah
źródło
2

W programie CodePlex można użyć CInject Framework open source . Możesz napisać minimalny kod, aby utworzyć wtryskiwacz i szybko przechwycić dowolny kod za pomocą CInject. Dodatkowo, ponieważ jest to Open Source, możesz to również rozszerzyć.

Możesz też wykonać kroki wymienione w tym artykule na temat przechwytywania wywołań metod przy użyciu języka IL i utworzyć własny przechwytywacz przy użyciu klas Reflection.Emit w języku C #.

Puneet Ghanshani
źródło
1

Nie znam rozwiązania, ale moje podejście wyglądałoby następująco.

Udekoruj klasę (lub jej metody) atrybutem niestandardowym. W innym miejscu programu pozwól funkcji inicjalizującej odzwierciedlać wszystkie typy, odczytaj metody ozdobione atrybutami i wstrzyknij kod IL do metody. W rzeczywistości może być bardziej praktyczne zastąpienie metody przez wywołanie kodu pośredniczącego LogStart, a następnie rzeczywistą metodę LogEnd. Ponadto nie wiem, czy można zmienić metody za pomocą odbicia, więc może być bardziej praktyczne zastąpienie całego typu.

Konrad Rudolph
źródło
1

Możesz potencjalnie użyć wzorca dekoratora GOF i „ozdobić” wszystkie klasy, które wymagają śledzenia.

Prawdopodobnie jest to naprawdę praktyczne tylko w przypadku kontenera IOC (ale jako wskazówka wcześniej możesz rozważyć przechwycenie metody, jeśli zamierzasz podążać ścieżką IOC).

Stacy A.
źródło
1

AOP jest koniecznością do implementacji czystego kodu, jednak jeśli chcesz otoczyć blok w języku C #, metody ogólne są stosunkowo łatwiejsze w użyciu. (z intelli sense i silnie wpisanym kodem) Z pewnością NIE może być alternatywą dla AOP.

Chociaż PostSHarp ma małe problemy z błędami (nie czuję się pewnie podczas używania w produkcji), jest to dobra rzecz.

Ogólna klasa opakowania,

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

użycie mogłoby wyglądać tak (oczywiście z intelli sense)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");
Nitro
źródło
Zgadzam się, że Postsharp jest biblioteką AOP i poradzi sobie z przechwytywaniem, jednak twój przykład niczego takiego nie ilustruje. Nie myl IoC z przechwytywaniem. One nie są takie same.
Opóźnienie
-1
  1. Napisz własną bibliotekę AOP.
  2. Użyj odbicia, aby wygenerować serwer proxy rejestrowania dla swoich instancji (nie jestem pewien, czy możesz to zrobić bez zmiany części istniejącego kodu).
  3. Przepisz zestaw i wstrzyknij swój kod logowania (w zasadzie taki sam jak 1).
  4. Hostuj CLR i dodaj rejestrowanie na tym poziomie (myślę, że jest to najtrudniejsze rozwiązanie do wdrożenia, ale nie jestem pewien, czy masz wymagane punkty zaczepienia w CLR).
kokos
źródło
-3

Najlepsze, co możesz zrobić przed wydaniem C # 6 z wydaniem „nameof”, to użycie powolnych wyrażeń StackTrace i linq.

Np. Za taką metodę

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

        //do your stuff
    }

Taka linia może pojawić się w Twoim pliku dziennika

Method 'MyMethod' parameters age: 20 name: Mike

Oto realizacja:

    //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
    }
iriss
źródło
To nie spełnia wymagania określonego w jego drugim podpunkcie.
Ted Bigham