156

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

假设:

  • 我不想为 C# 使用任何 3rd 方 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);
 }

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

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

4

16 回答 16

72

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

我寻找方法来做你想做的事,但我发现没有简单的方法去做。

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

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

为此,您有两个主要选择

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

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

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

于 2008-08-25T09:31:40.953 回答
49

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

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

于 2008-08-25T09:41:12.740 回答
11

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

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

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

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

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

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

于 2016-12-13T19:07:24.650 回答
7

如果您编写一个实现 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 ...]
            }
        }
    }
于 2008-08-25T09:12:02.920 回答
6

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

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

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

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

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

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

于 2016-12-13T20:42:14.583 回答
5

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

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

于 2008-08-25T09:12:17.133 回答
5

我找到了另一种可能更容易的方法...

声明一个方法 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 框架或从 MarshalByRefObject 派生或使用远程处理或代理类更轻。

于 2011-03-17T20:45:17.633 回答
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(); 
     }
 }
于 2015-08-26T23:40:45.563 回答
2

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

或者,您可以按照本文中提到的使用 IL 拦截方法调用的步骤操作,并使用 C# 中的 Reflection.Emit 类创建自己的拦截器。

于 2012-12-11T02:34:04.953 回答
1

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

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

于 2008-08-25T09:07:28.220 回答
1

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

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

于 2008-09-18T16:31:19.263 回答
1

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

于 2009-11-21T06:34:06.630 回答
1

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");
于 2011-05-12T22:45:09.503 回答
0

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

您要实现的目标是内置在 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

于 2020-08-04T05:02:00.303 回答
-1
  1. 编写自己的 AOP 库。
  2. 使用反射在您的实例上生成日志代理(不确定是否可以在不更改现有代码的某些部分的情况下做到这一点)。
  3. 重写程序集并注入您的日志记录代码(与 1 基本相同)。
  4. 托管 CLR 并在此级别添加日志记录(我认为这是最难实现的解决方案,但不确定 CLR 中是否有所需的挂钩)。
于 2008-08-25T09:06:09.487 回答
-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
    }
于 2015-07-12T03:44:07.310 回答