【问题标题】:How do I intercept a method call in C#?如何在 C# 中拦截方法调用?
【发布时间】:2008-08-25 09:00:23
【问题描述】:

对于给定的类,我希望具有跟踪功能,即我希望记录每个方法调用(方法签名和实际参数值)和每个方法退出(仅方法签名)。

我如何做到这一点假设:

  • 我不想使用任何第三方 C# 的 AOP 库,
  • 我不想将重复的代码添加到我要跟踪的所有方法中,
  • 我不想更改类的公共 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);
 }

如何在每次调用 Method1Method2 的情况下调用 Logger.LogStartLogger.LogEnd修改 Caller.Call 方法,而不将调用显式添加到 Traced.Method1Traced.Method2

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

【问题讨论】:

标签: c# reflection aop


【解决方案1】:

C# 不是面向 AOP 的语言。它有一些 AOP 特性,你可以模仿其他一些特性,但是用 C# 做 AOP 是很痛苦的。

我想尽办法去做你想做的事,但我没有找到简单的方法。

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

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

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

  1. 从 MarshalByRefObject 或 ContextBoundObject 继承您的类并定义从 IMessageSink 继承的属性。 This article 有一个很好的例子。尽管如此,您必须考虑到使用 MarshalByRefObject 性能会像地狱一样下降,我的意思是,我说的是性能损失了 10 倍,所以在尝试之前请仔细考虑。

  2. 另一种选择是直接注入代码。在运行时,这意味着您必须使用反射来“读取”每个类,获取其属性并注入适当的调用(就此而言,我认为您不能使用 Reflection.Emit 方法,因为我认为 Reflection.Emit 不会'不允许你在已经存在的方法中插入新代码)。在设计时,这将意味着创建 CLR 编译器的扩展,我真的不知道它是如何完成的。

最后一个选项是使用IoC framework。也许这不是完美的解决方案,因为大多数 IoC 框架通过定义允许挂钩方法的入口点来工作,但根据您想要实现的目标,这可能是一个公平的近似值。

【讨论】:

  • 我应该指出,如果你有一流的函数,那么一个函数可以像任何其他变量一样被对待,你可以有一个“方法挂钩”来做他想做的事。
  • 第三种选择是在运行时使用Reflection.Emit 生成基于继承的aop 代理。这是approach chosen by Spring.NET。但是,这需要Traced 上的虚拟方法,并且不适合在没有某种 IOC 容器的情况下使用,所以我理解为什么这个选项不在您的列表中。
  • 您的第二个选项基本上是“手动编写您需要的 AOP 框架的部分”,这应该会导致“哦,等等,也许我应该使用专门创建的第 3 方选项来解决我遇到的问题,而不是走不走这条路”
  • 知道了,使用依赖注入拦截:stackoverflow.com/questions/12083052/…
【解决方案2】:

实现这一点的最简单方法可能是使用PostSharp。它根据您应用的属性在您的方法中注入代码。它可以让你做你想做的事。

另一种选择是使用profiling API 在方法内注入代码,但这确实是硬核。

【讨论】:

  • 你也可以用 ICorDebug 注入东西,但这太邪恶了
【解决方案3】:

您可以使用 DI 容器的 Interception 功能来实现它,例如 Castle Windsor。实际上,可以通过这样的方式来配置容器,即每个具有由特定属性修饰的方法的类都会被拦截。

关于第 3 点,OP 要求提供没有 AOP 框架的解决方案。我在下面的答案中假设应该避免的是 Aspect、JointPoint、PointCut 等。根据Interception documentation from 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");
    }
}

将 logging 属性添加到您的方法以进行日志记录

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

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

请注意,如果只需要拦截类的某些方法,则需要对属性进行一些处理。默认情况下,所有公共方法都会被拦截。

【讨论】:

    【解决方案4】:

    如果你编写了一个实现 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 ...]
                }
            }
        }
    

    【讨论】:

    • 看起来很辛苦
    • 这与回答问题无关。
    • 这叫做“复制粘贴”
    【解决方案5】:

    如果你想不受限制地跟踪你的方法(没有代码适配,没有 AOP 框架,没有重复代码),让我告诉你,你需要一些魔法......

    说真的,我解决了它来实现一个在运行时工作的 AOP 框架。

    你可以在这里找到:NConcern .NET AOP Framework

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

    如果您不想使用 3rd 方程序集,您可以浏览代码源(开源)并复制两个文件 Aspect.Directory.csAspect.Directory.Entry.cs 以根据您的意愿进行调整。这些类允许在运行时替换您的方法。我只是要求你尊重许可证。

    我希望你能找到你需要的东西,或者说服你最终使用 AOP 框架。

    【讨论】:

      【解决方案6】:

      看看这个 - 很重的东西.. http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

      Essential .net - don box 有一章关于您需要什么,称为拦截。 我在这里刮了一些(对不起字体颜色 - 当时我有一个黑暗的主题......) http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html

      【讨论】:

        【解决方案7】:

        我找到了一种可能更简单的方法...

        声明一个方法调用方法

        [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 派生或使用远程处理或代理类更轻。

        【讨论】:

          【解决方案8】:

          首先你必须修改你的类来实现一个接口(而不是实现 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(); 
               }
           }
          

          【讨论】:

            【解决方案9】:

            您可以在 CodePlex 上使用开源框架 CInject。您可以编写最少的代码来创建一个 Injector,并让它使用 CInject 快速拦截任何代码。另外,由于这是开源的,你也可以扩展它。

            或者您可以按照本文Intercepting Method Calls using IL 中提到的步骤,使用 C# 中的 Reflection.Emit 类创建自己的拦截器。

            【讨论】:

              【解决方案10】:

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

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

              【讨论】:

                【解决方案11】:

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

                这可能只对 IOC 容器真正实用(但正如前面指出的那样,如果您打算沿着 IOC 路径前进,您可能需要考虑方法拦截)。

                【讨论】:

                  【解决方案12】:

                  您需要向 Ayende 询问他是如何做到的: http://ayende.com/Blog/archive/2009/11/19/can-you-hack-this-out.aspx

                  【讨论】:

                    【解决方案13】:

                    AOP 是实现干净代码的必要条件,但是如果你想在 C# 中包围一个块,泛型方法相对更容易使用。 (具有智能感知和强类型代码)当然,它不能替代 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 与拦截混淆。它们不一样。
                    【解决方案14】:

                    也许这个答案为时已晚,但它就在这里。

                    您希望实现的目标是内置在 MediatR 库中。

                    这是我的 RequestLoggerBehaviour,它拦截对我的业务层的所有调用。

                    namespace SmartWay.Application.Behaviours
                    {
                        public class RequestLoggerBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
                        {
                            private readonly ILogger _logger;
                            private readonly IAppSession _appSession;
                            private readonly ICreateLogGrain _createLogGrain;
                    
                            public RequestLoggerBehaviour(ILogger<TRequest> logger, IAppSession appSession, IClusterClient clusterClient)
                            {
                                _logger = logger;
                                _appSession = appSession;
                                _createLogGrain = clusterClient.GetGrain<ICreateLogGrain>(Guid.NewGuid());
                            }
                    
                            public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
                            {
                                var name = typeof(TRequest).Name;
                                _logger.LogInformation($"SmartWay request started: ClientId: {_appSession.ClientId} UserId: {_appSession.UserId} Operation: {name} Request: {request}");
                    
                                var response = await next();
                    
                                _logger.LogInformation($"SmartWay request ended: ClientId: {_appSession.ClientId} UserId: {_appSession.UserId} Operation: {name} Request: {request}");
                    
                                return response;
                            }
                        }
                    }
                    

                    例如,您还可以创建性能行为来跟踪执行时间过长的方法。

                    在您的业务层上拥有干净的架构 (MediatR) 将使您在执行 SOLID 原则的同时保持代码干净。

                    你可以在这里看到它是如何工作的: https://youtu.be/5OtUm1BLmG0?t=1

                    【讨论】:

                      【解决方案15】:
                      1. 编写您自己的 AOP 库。
                      2. 使用反射在您的实例上生成日志代理(不确定是否可以在不更改现有代码的某些部分的情况下做到这一点)。
                      3. 重写程序集并注入您的日志记录代码(与 1 基本相同)。
                      4. 托管 CLR 并在此级别添加日志记录(我认为这是最难实现的解决方案,但不确定 CLR 中是否有所需的挂钩)。

                      【讨论】:

                        【解决方案16】:

                        在发布“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
                            }
                        

                        【讨论】:

                        • 这不符合他在第二个要点中定义的要求。
                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 2015-04-29
                        • 2010-12-09
                        • 1970-01-01
                        • 1970-01-01
                        • 2021-09-16
                        • 2014-10-11
                        相关资源
                        最近更新 更多