C#でメソッド呼び出しをインターセプトするにはどうすればよいですか?


154

特定のクラスについて、トレース機能を使用したいと考えています。つまり、すべてのメソッド呼び出し(メソッドシグネチャと実際のパラメーター値)とすべてのメソッド出口(メソッドシグネチャのみ)をログに記録したいと考えています。

次のことを前提としてこれをどのように達成しますか:

  • 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);
 }

どうすれば起動しない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 2017年

回答:


69

C#はAOP指向の言語ではありません。AOPの機能がいくつかあり、他の機能をエミュレートできますが、C#でAOPを作成するのは面倒です。

私はあなたがやりたいことを正確に行う方法を探しましたが、簡単な方法が見つかりませんでした。

私が理解しているように、これはあなたがしたいことです:

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

そのためには、2つの主なオプションがあります。

  1. MarshalByRefObjectまたはContextBoundObjectからクラスを継承し、IMessageSinkから継承する属性を定義します。この記事には良い例があります。それにもかかわらず、MarshalByRefObjectを使用するとパフォーマンスが地獄のように低下​​することを考慮する必要があります。つまり、10倍のパフォーマンスの低下について話しているので、それを試す前によく考えてください。

  2. もう1つのオプションは、コードを直接挿入することです。ランタイムでは、リフレクションを使用してすべてのクラスを「読み取り」、その属性を取得し、適切な呼び出しを注入する必要があります(そして、私はReflection.Emitが考えているように、Reflection.Emitメソッドを使用できなかったと思います)既存のメソッド内に新しいコードを挿入することはできません)。設計時には、これはCLRコンパイラーへの拡張を作成することを意味します。

最後のオプションは、IoCフレームワークを使用することです。ほとんどのIoCフレームワークは、メソッドのフックを可能にするエントリポイントを定義することで機能するため、完全なソリューションではないかもしれませんが、達成したいものによっては、かなり近似している可能性があります。


62
つまり、 'ouch'
johnc 2008

2
最初のクラスの関数がある場合、関数は他の変数と同じように扱われ、彼が望むことを実行する「メソッドフック」を持つことができることを指摘しておきます。
RCIX、2009

3
3つ目の方法は、を使用して、実行時に継承ベースのaopプロキシを生成することですReflection.Emit。これはSpring.NETによって選択されアプローチです。ただし、これには仮想メソッドをオンにする必要があり、TracedなんらかのIOCコンテナーなしでの使用には実際には適していないため、このオプションがリストにない理由を理解しています。
Marijn

2
あなたの2番目のオプションは基本的に「手で必要なAOPフレームワークの部分を書く」です、そしてそれは「ああ待って多分私はダウンしないのではなく、私が持っている問題を解決するために特別に作成されたサードパーティオプションを使用すべきです-inveted-here-road "
Rune FS

2
@jorge nInjectなどのDependency Injection / IoCのフレームワークを使用してこれを達成するための例/リンクを提供できますか
Charanraj Golla

48

これを実現する最も簡単な方法は、おそらくPostSharpを使用することです。適用する属性に基づいて、メソッド内にコードを挿入します。それはあなたが望むものを正確に行うことを可能にします。

別のオプションは、プロファイリングAPIを使用してメソッド内にコードを挿入することですが、それは本当にハードコアです。


3
ICorDebugを使用してコンテンツを注入することもできますが、それは非常に悪いことです
Sam Saffron

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

Castle Windsorなどの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 Framework

このようなニーズに応えるために、このAOPフレームワークを作成することにしました。それは非常に軽量なシンプルなライブラリです。ロガーの例をホームページでご覧いただけます。

サードパーティのアセンブリを使用したくない場合は、コードソース(オープンソース)を参照し、Aspect.Directory.csAspect.Directory.Entry.csの両方のファイルをコピーして、必要応じて変更できます。これらのクラスにより、実行時にメソッドを置き換えることができます。ライセンスを尊重するようお願いします。

必要なものが見つかるか、AOPフレームワークを最終的に使用するように説得していただければ幸いです。



4

私はもっ​​と簡単かもしれない別の方法を見つけました...

メソッド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から派生するか、リモートまたはプロキシクラスを使用することに同意することを期待します。


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(); 
     }
 }

2

CodePlexでオープンソースフレームワークCInjectを使用できます。最小限のコードを記述してインジェクターを作成し、CInjectを使用してコードをすばやくインターセプトすることができます。さらに、これはオープンソースなので、これを拡張することもできます。

または、IL使用したメソッド呼び出しのインターセプトに関するこの記事で説明されている手順に従い、C#のReflection.Emitクラスを使用して独自のインターセプターを作成できます。


1

解決策はわかりませんが、私のアプローチは次のようになります。

クラス(またはそのメソッド)をカスタム属性で装飾します。プログラムの他の場所で、初期化関数にすべてのタイプを反映させ、属性で装飾されたメソッドを読み取り、ILコードをメソッドに挿入します。実際にメソッドを呼び出してから、メソッドを呼び出すスタブでメソッドを置き換えるほうLogStartが実際的である場合がありLogEndます。さらに、リフレクションを使用してメソッドを変更できるかどうかはわからないため、タイプ全体を置き換える方が現実的かもしれません。


1

GOFデコレータパターンを使用して、トレースが必要なすべてのクラスを「装飾」することができます。

これは、IOCコンテナーでのみ実際に実用的です(ただし、先に指摘したように、IOCパスを使用する場合は、メソッドのインターセプトを検討することをお勧めします)。



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

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
    }

これは、彼の2番目の箇条書きで定義された要件を満たしていません。
Ted Bigham、2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.