Як перехопити виклик методу в C #?


154

Для даного класу я хотів би мати функцію відстеження, тобто я хотів би записати кожен виклик методу (підпис методу та фактичні значення параметрів) та кожен вихід методу (лише підпис методу).

Як мені це зробити, якщо:

  • Я не хочу використовувати жодні сторонні бібліотеки AOP для C #,
  • Я не хочу додавати дублікат коду до всіх методів, які я хочу простежити,
  • Я не хочу змінювати публічний API класу - користувачі класу повинні мати можливість викликати всі методи точно так само.

Щоб зробити питання більш конкретним, припустимо, є 3 класи:

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

Як я можу викликати Logger.LogStart та Logger.LogEnd для кожного виклику до Method1 та Method2 без зміни методу Caller.Call та без явного додавання викликів до Traced.Method1 та Traced.Method2 ?

Редагувати: Що було б рішенням, якщо мені дозволять трохи змінити метод виклику?



1
Якщо ви хочете знати, як працює перехоплення у C #, подивіться на Tiny Interceptor . Цей зразок працює без будь-яких залежностей. Зауважте, що якщо ви хочете використовувати AOP в реальних проектах, не намагайтеся реалізувати його самостійно. Використовуйте такі бібліотеки, як PostSharp.
Джалал

Я реалізував журнал виклику методу (до і після) за допомогою бібліотеки MethodDecorator.Fody. Перегляньте бібліотеку на сайті github.com/Fody/MethodDecorator
Dilhan Jayathilake

Відповіді:


69

C # не є орієнтованою на АОП мовою. Він має деякі функції AOP, і ви можете імітувати деякі інші, але зробити AOP за допомогою C # болісно.

Я шукав способи зробити саме те, що ти хотів зробити, і не знайшов простого способу це зробити.

Як я розумію, це ви хочете зробити:

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

і для цього у вас є два основні варіанти

  1. Успадкуйте свій клас від MarshalByRefObject або ContextBoundObject і визначте атрибут, який успадковується від IMessageSink. У цій статті є хороший приклад. Ви все-таки повинні врахувати, що використання MarshalByRefObject продуктивність знизиться як пекло, і я маю на увазі це, я говорю про 10-кратну втрату продуктивності, тому ретельно подумайте, перш ніж спробувати це.

  2. Інший варіант - ввести код безпосередньо. Під час виконання, це означає, що вам доведеться використовувати роздуми, щоб "прочитати" кожен клас, отримати його атрибути та ввести відповідний виклик (і з цього приводу я думаю, ви не могли використовувати метод Reflection.Emit, як я думаю, що Reflection.Emit wouldn не дозволяю вставити новий код у вже існуючий метод). На час розробки це означатиме створення розширення до компілятора CLR, я, чесно кажучи, не маю уявлення про те, як це робиться.

Остаточний варіант - використання фреймворку IoC . Можливо, це не ідеальне рішення, оскільки більшість фреймворків IoC працює, визначаючи точки входу, які дозволяють приєднати методи, але, в залежності від того, що ви хочете досягти, це може бути справедливим методом прихильності.


62
Іншими словами, 'ouch'
johnc

2
Я мушу зазначити, що якби у вас були функції першого класу, то до функції можна було б ставитися так само, як до будь-якої іншої змінної, і у вас може бути "гачок методу", який робить те, що він хоче.
RCIX

3
Третя альтернатива - це генерування Aop проксі-серверів на основі спадкування під час виконання Reflection.Emit. Це підхід, обраний Spring.NET . Однак для цього знадобляться віртуальні методи Traced, які не підходять для використання без якогось IOC-контейнера, тому я розумію, чому ця опція відсутня у вашому списку.
Марійн

2
ваш другий варіант, в основному, "Напишіть потрібні вам частини AOP-рамки вручну", що потім призведе до висновку "О, зачекайте, можливо, я повинен використовувати сторонній варіант, створений спеціально для вирішення проблеми, яку я маю замість того, щоб знижуватись не" -вкладено-тут-дорога "
Rune FS

2
@jorge Чи можете ви надати якийсь приклад / посилання, щоб досягти цього, використовуючи Dependency Injection / IoC Famework, як nInject
Charanraj Golla

48

Найпростіший спосіб цього досягти - це, мабуть, використання PostSharp . Він вводить код усередині ваших методів на основі атрибутів, які ви застосовуєте до нього. Це дозволяє робити саме те, що ви хочете.

Іншим варіантом є використання API профілювання для введення коду всередину методу, але це справді важко.


3
ви також можете вводити речі за допомогою ICorDebug, але це супер зло
Сем

9

Якщо ви пишете клас - називайте його Tracing -, який реалізує інтерфейс IDisposable, ви можете обернути всі методи методу в

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

У класі Tracing ви можете обробити логіку слідів у конструкторі / Dispose методі відповідно в класі Tracing для відстеження введення та виходу методів. Такий, що:

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

виглядає як багато зусиль
LeRoi

3
Це не має нічого спільного з відповіддю на запитання.
Затримка

9

Ви можете досягти цього за допомогою функції перехоплення контейнера DI, такого як Castle Windsor . Дійсно, можна настроїти контейнер таким чином, щоб усі класи, які мають метод, прикрашений певним атрибутом, перехоплювалися.

Щодо пункту №3, ОП просила рішення без рамки АОП. Я припустив , в наступному відповіді, що слід уникати були Аспект, JointPoint, Pointcut і т.д. Відповідно до перехоплення документації від CastleWindsor , жоден з тих , які необхідні для досягнення того, що просять.

Налаштуйте загальну реєстрацію перехоплювача на основі наявності атрибута:

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

Додайте створену IContributeComponentModelConstruction до контейнера

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

І ви можете робити все, що завгодно, в самому перехоплювачі

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

Додайте атрибут журналу до свого методу для ведення журналу

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

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

Зауважте, що деяке поводження з атрибутом буде потрібно, якщо потрібно перехопити лише якийсь метод класу. За замовчуванням всі публічні методи будуть перехоплені.


5

Якщо ви хочете простежити за своїми методами без обмежень (без адаптації коду, без AOP Framework, без дублікату коду), дозвольте сказати, вам потрібна якась магія ...

Серйозно, я вирішив це застосувати AOP Framework, що працює під час виконання.

Ви можете знайти тут: NConcern .NET AOP Framework

Я вирішив створити цю рамку AOP, щоб відповісти на такі потреби. це проста бібліотека дуже легкої ваги. Приклад реєстратора можна побачити на домашній сторінці.

Якщо ви не хочете використовувати сторонні збори, ви можете переглянути джерело коду (відкритий код) та скопіювати обидва файли Aspect.Directory.cs та Aspect.Directory.Entry.cs, адаптовані під ваші побажання. Класи тези дозволяють замінити ваші методи під час виконання. Я просто попросив би вас поважати ліцензію.

Я сподіваюся, що ви знайдете те, що вам потрібно, або переконаєте вас нарешті використовувати AOP Framework.


4

Погляньте на це - досить важкі речі .. http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

Essential .net - don box мав розділ про те, що вам потрібно називати Перехоплення. Деякі з них я викреслив тут (Вибачте за кольори шрифту - у мене тоді була темна тема ...) http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html


4

Я знайшов інший спосіб, який може бути простішим ...

Оголосити метод 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;
        }
    }

Потім я так визначаю свої методи

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

Тепер я можу перевірити час виконання без введення залежності ...

На сайті немає місць :)

Сподіваємось, ви погодиться з тим, що це менша вага, ніж AOP Framework або походить від MarshalByRefObject або використовує видалення або проксі-класи.


4

Спочатку ви повинні змінити свій клас для реалізації інтерфейсу (а не для реалізації MarshalByRefObject).

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

Далі вам потрібен загальний об’єкт обгортки на основі RealProxy, щоб прикрасити будь-який інтерфейс, щоб дозволити перехопити будь-який виклик декорованого об’єкта.

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

Тепер ми готові перехопити виклики до Method1 та Method2 з ITraced

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

2

Можна використовувати рамку CInject з відкритим кодом з на CodePlex. Ви можете написати мінімальний код, щоб створити інжектор, і змусити його швидко перехопити будь-який код за допомогою CInject. Плюс, оскільки це Open Source, ви також можете продовжити це.

Або ви можете виконати кроки, згадані в цій статті про перехоплення методів викликів за допомогою IL та створити власний перехоплювач за допомогою Reflection.Emit класів у C #.


1

Я не знаю рішення, але мій підхід був би наступним.

Прикрасьте клас (або його методи) спеціальним атрибутом. Десь в програмі нехай функція ініціалізації відображає всі типи, читає методи, прикрашені атрибутами, і вводить у метод якийсь код IL. Фактично може бути більш практичним замінити метод на заглушку, яка викликає LogStart, власне метод і потімLogEnd . Крім того, я не знаю, чи можна змінити методи за допомогою рефлексії, щоб замінити весь тип може бути більш практичним.


1

Ви потенційно можете використовувати шаблон декоратора GOF та "прикрасити" всі класи, які потребують відстеження.

Це, мабуть, справді практично з контейнером МОК (але, як вказувати раніше, ви можете розглянути можливість перехоплення методу, якщо ви збираєтеся спуститися по шляху МОК).



1

Для впровадження чистого коду AOP є обов'язковим, однак, якщо ви хочете оточити блок в C #, загальні методи мають порівняно простіше використання. (з розумінням Intelli та сильно набраним кодом) Звичайно, це не може бути альтернативою AOP.

Хоча PostSHarp проблеми з помилками (я не впевнений у використанні на виробництві), це хороший матеріал.

Загальний клас обгортки,

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

використання може бути таким (з розумом, зрозуміло)

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

Я погоджуюсь, що Postsharp - це бібліотека AOP і буде обробляти перехоплення, однак ваш приклад не ілюструє нічого подібного. Не плутайте IoC з перехопленням. Вони не однакові.
Затримка

-1
  1. Напишіть власну бібліотеку AOP.
  2. Використовуйте відображення для створення проксі-журналу для ваших примірників (не впевнений, чи можна це зробити, не змінюючи частину наявного коду).
  3. Перезапишіть збірку та вставте свій код реєстрації (в основному такий же, як 1).
  4. Розмістіть CLR і додайте журнал на цьому рівні (я думаю, що це найскладніше рішення для реалізації, не впевнений, чи є у вас потрібні гачки в CLR, хоча).

-3

Найкраще, що ви можете зробити перед C # 6 із випуском 'nameof' - це використовувати повільні StackTrace та linq вирази.

Наприклад, для такого методу

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

        //do your stuff
    }

Такий рядок може бути створений у вашому файлі журналу

Method 'MyMethod' parameters age: 20 name: Mike

Ось реалізація:

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

Це не відповідає вимозі, визначеній у його другій кулі.
Тед Бігхем
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.