如何拦截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.LogStartLogger.LogEnd每次调用方法1方法2,而无需修改Caller.Call方法,没有明确地加入调用Traced.Method1Traced.Method2

编辑:如果允许我稍微更改Call方法,那将是解决方案?



1
如果您想知道拦截在C#中是如何工作的,请看一下Tiny Interceptor。该示例运行时没有任何依赖性。请注意,如果要在实际项目中使用AOP,请不要尝试自己实现它。使用PostSharp之类的库。
Jalal

我已经使用MethodDecorator.Fody库实现了方法调用的记录(之前和之后)。请在github.com/Fody/MethodDecorator上
Dilhan Jayathilake

Answers:


69

C#不是面向AOP的语言。它具有一些AOP功能,您可以模仿其他一些功能,但是使用C#进行AOP却很痛苦。

我一直在寻找可以完全按照自己的意愿去做的方法,但发现没有简单的方法可以做到。

据我了解,这是您想要做的:

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

为了做到这一点,您有两个主要选择

  1. 从MarshalByRefObject或ContextBoundObject继承您的类,并定义一个从IMessageSink继承的属性。本文有一个很好的例子。尽管如此,您仍然必须考虑使用MarshalByRefObject会导致性能下降,而我的意思是,我说的是性能下降了10倍,因此在尝试之前,请仔细考虑。

  2. 另一种选择是直接注入代码。在运行时中,这意味着您必须使用反射来“读取”每个类,获取其属性并注入适当的调用(为此,我认为您不能像我认为的那样使用Reflection.Emit方法。不允许您在现有方法中插入新代码)。在设计时,这将意味着创建CLR编译器的扩展,老实说,我不知道该如何完成。

最后的选择是使用IoC框架。也许这不是一个完美的解决方案,因为大多数IoC框架通过定义允许连接方法的入口点来工作,但是根据您要实现的目标,这可能是一个合理的替代。


62
换句话说,“哎呀”
约翰·约翰逊

2
我应该指出,如果您具有一流的函数,那么可以像对待其他任何变量一样对待函数,并且可以使用一个“方法挂钩”来实现他想要的功能。
RCIX

3
第三种选择是在运行时使用生成基于继承的aop代理Reflection.Emit。这是Spring.NET选择方法。但是,这将需要使用虚拟方法,Traced并且如果没有某种IOC容器,则不适合使用,因此我知道为什么此选项不在您的列表中。
Marijn 2011年

2
您的第二个选择基本上是“手工编写所需的AOP框架的各个部分”,这应得出“哦,也许我应该使用专门创建的第3方选择来解决我遇到的问题,而不是失败。 -inveted-here-road”
符文FS

2
@jorge您能提供一些示例/链接来使用依赖注入/ IoC框架来实现此功能吗,例如nInject
Charanraj Golla,

48

最简单的方法可能是使用PostSharp。它根据您应用于它的属性将代码注入到您的方法中。它使您可以完全按照自己的意愿做。

另一个选择是使用性能分析API将代码注入方法中,但这确实很困难。


3
你也可以注入与ICorDebug但东西,是超邪恶
萨姆藏红花

9

如果您编写一个实现IDisposable接口的类-称为Tracing-,则可以将所有方法主体包装在一个

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

在Tracing类中,可以在Tracing类中分别处理构造函数/ 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 ...]
            }
        }
    }

看起来很费劲
LeRoi

3
这与回答问题无关。
延迟

9

您可以通过DI容器(例如温莎城堡)的拦截功能来实现。实际上,可以以这样的方式配置容器,使得具有由特定属性修饰的方法的每个类都将被拦截。

关于第3点,OP要求没有AOP框架的解决方案。我在以下答案中假定应该避免使用Aspect,JointPoint,PointCut等。根据CastleWindsor的Interception文档,完成这些任务不需要这些。

根据属性的存在,配置拦截器的通用注册:

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框架,无重复代码),请告诉我,您需要一些魔术...

认真地说,我解决了它,以实现在运行时工作的AOP框架。

您可以在这里找到:NConcern .NET AOP框架

我决定创建此AOP框架来响应这种需求。这是一个非常轻量的简单库。您可以在主页上看到记录器的示例。

如果您不想使用第三方程序集,则可以浏览代码源(开放源代码),并复制文件Aspect.Directory.csAspect.Directory.Entry.cs以适应您的需要。这些类允许在运行时替换您的方法。我只想请您尊重许可。

我希望您能找到所需的内容或说服您最终使用AOP框架。



4

我发现了另一种可能更容易的方式...

声明方法调用方法

[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框架,从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);
    }
}

现在我们准备拦截对ITraced的Method1和Method2的调用

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


1

我不知道解决方案,但是我的方法如下。

用自定义属性装饰类(或其方法)。在程序中的其他位置,让初始化函数反映所有类型,读取用属性装饰的方法,并将一些IL代码注入该方法。实际上,用调用实际方法然后使用的存根替换方法可能更实际。另外,我不知道您是否可以使用反射更改方法,因此替换整个类型可能更实用。LogStartLogEnd


1

您可能会使用GOF装饰器模式,并“装饰”所有需要跟踪的类。

这可能仅在IOC容器中才真正实用(但如前所述,如果您打算沿IOC路径前进,则可能需要考虑方法拦截)。



1

AOP是实现干净代码的必要条件,但是,如果要在C#中包围一个块,则泛型方法的使用相对容易。(具有智能意义和强类型代码)当然,它不能替代AOP。

尽管PostSHarp几乎没有bug问题(我对在生产环境中使用时没有信心),但这是个好东西。

通用包装器类,

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

在发布带有“ nameof”的C#6之前,您可以做的最好的事情就是使用慢速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
    }

这没有满足他第二个要点中定义的要求。
泰德·比格姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.