Как перехватить вызов метода в C#?


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

Как мне это сделать, предполагая, что:

  • Я не хочу использовать какую-либо третью сторону Библиотеки 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);
 }

Как мне вызвать регистратор.LogStart и регистратор.LogEnd для каждого вызова Method1 и метода Method2 без изменения абонент.Звоните метод и без добавления вызовов явно прослеживается.Метод1 и прослеживается.Метода Method2?

Edit: каким будет решение, если мне разрешат немного изменить метод вызова?

15 139

15 ответов:

C# не является языком, ориентированным на AOP. Он имеет некоторые функции AOP, и вы можете эмулировать некоторые другие, но создание AOP с C# является болезненным.

Я искал способы сделать именно то, что вы хотели сделать, и я не нашел простого способа сделать это.

Как я понял, это то, что вы хотите сделать:

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

и для этого у вас есть два основных варианта

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

  2. другой вариант-ввести код напрямую. Во время выполнения, то есть вам придется использовать отражение, чтобы "прочитать" каждый класс, получить его атрибуты и ввести соответствующий вызов (и в этом отношении я думаю, что вы не могли бы использовать отражение.Испускают метод как я думаю отражение.Emit не позволит вам вставлять новый код внутри уже существующего метода). Во время разработки это будет означать создание расширения для компилятора CLR, который я честно не знаю, как это делается.

последний вариант использует рамках МОК. Возможно, это не идеальное решение, поскольку большинство фреймворков МОК работает, определяя точки входа которые позволяют подключать методы, но, в зависимости от того, что вы хотите достичь, это может быть справедливым приближением.

самый простой способ добиться этого, вероятно, использовать PostSharp. Он вводит код внутри ваших методов на основе атрибутов, которые вы применяете к нему. Она позволяет делать именно то, что вы хотите.

другой вариант-использовать профилирование API чтобы ввести код внутри метода, но это действительно хардкор.

Если вы пишете класс-назовите его трассировкой-который реализует интерфейс IDisposable, вы можете обернуть все тела метода в

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

в классе трассировки вы можете обрабатывать логику трассировок в методе конструктора / Dispose, соответственно, в классе трассировки, чтобы отслеживать вход и выход из методов. Такие, что:

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

вы могли бы достичь этого с Перехват особенность-Ди-контейнер, такие как Замок Виндзор. Действительно, можно настроить контейнер таким образом, что каждый класс, который имеет метод, оформленный определенным атрибутом, будет перехвачен.

что касается пункта № 3, OP попросил решение без AOP framework. В следующем ответе я предположил, что следует избегать аспекта, совместной точки, точечного разреза и т. д. В соответствии с Перехват документации от 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) { }
 }

обратите внимание, что некоторая обработка атрибута потребуется, если только какой-то метод класса должен быть перехвачен. По умолчанию все открытые методы будут перехвачены.

взгляните на это - довольно тяжелый материал.. http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

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

Я нашел другой путь, который может быть проще...

объявить метод 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;
    }

теперь я могу иметь проверку во время выполнения без инъекции зависимости...

нет gotchas в сайте:)

надеюсь, вы согласитесь, что это меньше веса, чем AOP Framework или производные от MarshalByRefObject или с помощью удаленного доступа или прокси-классов.

сначала вы должны изменить свой класс для реализации интерфейса (а не реализации 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(); 
     }
 }

Если вы хотите проследить за своими методами без ограничений (без адаптации кода, без рамки AOP, без дубликата кода), позвольте мне сказать вам, вам нужна магия...

серьезно, я решил реализовать AOP Framework, работающий во время выполнения.

вы можете найти здесь : Nconcern .NET AOP Framework

Я решил создать эту структуру AOP, чтобы дать ответ на такого рода потребности. это простая библиотека очень легкий. Вы можете увидеть пример регистратора на главной странице.

Если вы не хотите использовать стороннюю сборку, вы можете просмотреть исходный код (с открытым исходным кодом) и скопировать оба файла аспект.Справочник.cs и аспект.Справочник.Вхождение.cs адаптироваться по вашему желанию. Эти классы позволяют заменить ваши методы во время выполнения. Я бы просто попросил вас уважать лицензию.

Я надеюсь, что вы найдете то, что вам нужно или убедить вас, наконец, использовать структуру AOP.

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

украсьте класс (или его методы) пользовательским атрибутом. Где-то еще в программе Пусть функция инициализации отражает все типы, читает методы, украшенные атрибутами, и вводит в метод некоторый код IL. Это может быть на самом деле более практично заменить метод с помощью заглушки, которая вызывает LogStart, фактический метод, а затем LogEnd. Кроме того, я не знаю, можете ли вы измените методы с помощью отражения, чтобы было более практично заменить весь тип.

AOP является обязательным для реализации чистого кода, однако если вы хотите окружить блок в C#, универсальные методы имеют относительно более простое использование. (с intelli sense и строго типизированным кодом) конечно, это не может быть альтернативой для 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);
            }
        });
    }
}

использование может быть таким (с intelli смысле, конечно)

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

вы можете использовать open source framework модема, это актуально тем на CodePlex. Вы можете написать минимальный код для создания инжектора и заставить его быстро перехватывать любой код с помощью CInject. Кроме того, поскольку это открытый исходный код, вы также можете расширить его.

или вы можете выполнить шаги, упомянутые в этой статье на перехват вызовов метода с помощью IL и создать свой собственный перехватчик с помощью отражения.Выделять классы в C#.

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

вероятно, это действительно практично только с контейнером IOC (но как указатель ранее вы можете рассмотреть метод перехвата, если вы собираетесь спуститься по пути IOC).

вы должны ошибка Айенде для ответа о том, как он это сделал: http://ayende.com/Blog/archive/2009/11/19/can-you-hack-this-out.aspx

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

лучшее, что вы можете сделать до выхода 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
    }