簡體   English   中英

如何攔截 C# 中的方法調用?

[英]How do I intercept a method call in C#?

對於給定的類,我希望具有跟蹤功能,即我希望記錄每個方法調用(方法簽名和實際參數值)和每個方法退出(僅方法簽名)。

假設我如何做到這一點:

  • 我不想為 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);
 }

我如何調用Logger.LogStartLogger.LogEnd每次調用方法1方法2,而無需修改Caller.Call方法,沒有明確地加入調用Traced.Method1Traced.Method2?

編輯:如果允許我稍微更改 Call 方法,那么解決方案是什么?

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 框架通過定義允許方法被掛鈎的入口點來工作,但是,根據您想要實現的目標,這可能是一個公平的近似值。

實現這一目標的最簡單方法可能是使用PostSharp 它根據您應用的屬性在您的方法中注入代碼。 它使您可以完全按照自己的意願行事。

另一種選擇是使用分析 API在方法中注入代碼,但這確實是核心問題。

您可以使用 DI 容器(例如Castle Windsor )的攔截功能來實現它。 實際上,可以以這樣的方式配置容器,即攔截每個具有由特定屬性修飾的方法的類。

關於第 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");
    }
}

將 logging 屬性添加到您的方法中以進行記錄

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

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

請注意,如果只需要攔截某個類的某些方法,則需要對該屬性進行一些處理。 默認情況下,所有公共方法都會被攔截。

如果您編寫一個實現 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 ...]
            }
        }
    }

如果你想無限制地追蹤你的方法(沒有代碼改編,沒有 AOP 框架,沒有重復的代碼),讓我告訴你,你需要一些魔法......

說真的,我解決了它以實現在運行時工作的 AOP 框架。

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

我決定創建這個 AOP 框架來響應這種需求。 它是一個非常輕量級的簡單庫。 您可以在主頁中看到記錄器的示例。

如果您不想使用第 3 方程序集,您可以瀏覽代碼源(開源)並復制兩個文件Aspect.Directory.csAspect.Directory.Entry.cs以根據您的意願進行調整。 這些類允許在運行時替換您的方法。 我只是要求你尊重許可證。

我希望你能找到你需要的東西或說服你最終使用 AOP 框架。

看看這個 - 相當沉重的東西.. http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

Essential .net - don box 有一章介紹了您需要的內容,稱為攔截。 我在這里刮了一些(對不起字體顏色 - 當時我有一個黑暗的主題......) http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html

首先,您必須修改您的類以實現一個接口(而不是實現 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(); 
     }
 }

我找到了一種可能更容易的不同方法......

聲明一個方法 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 派生或使用遠程處理或代理類更輕。

您可以在 CodePlex 上使用開源框架CInject 您可以編寫最少的代碼來創建一個注入器,並使用 CInject 讓它快速攔截任何代碼。 另外,由於這是開源的,您也可以擴展它。

或者,您可以按照使用 IL 攔截方法調用的這篇文章中提到的步驟操作,並使用 C# 中的 Reflection.Emit 類創建您自己的攔截器。

你需要給 Ayende 找一個關於他是如何做到的答案: http : //ayende.com/Blog/archive/2009/11/19/can-you-hack-this-out.aspx

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

使用自定義屬性裝飾類(或其方法)。 在程序的其他地方,讓一個初始化函數反映所有類型,讀取用屬性修飾的方法並將一些 IL 代碼注入到方法中。 將方法替換為調用LogStart的存根、實際方法然后是LogEnd可能更實用。 此外,我不知道您是否可以使用反射更改方法,因此替換整個類型可能更實用。

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

您可以潛在地使用 GOF 裝飾器模式,並“裝飾”所有需要跟蹤的類。

它可能只適用於 IOC 容器(但作為早先指出的指針,如果您要沿着 IOC 路徑走下去,您可能需要考慮方法攔截)。

也許這個答案為時已晚,但它就在這里。

您希望實現的目標是內置於 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

  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
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM