C# でメソッド呼び出しをインターセプトするにはどうすればよいですか?
-
09-06-2019 - |
質問
特定のクラスに対して、トレース機能が必要です。すべてのメソッド呼び出し (メソッド シグネチャと実際のパラメータ値) とすべてのメソッド終了 (メソッド シグネチャのみ) をログに記録したいと考えています。
次のことを前提としてこれを実現するにはどうすればよいですか:
- 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.LogStart そして Logger.LogEnd 電話をかけるたびに 方法1 そして 方法2 を変更せずに、 発信者.コール メソッドに明示的に呼び出しを追加せずに、 トレースされたメソッド 1 そして トレースされたメソッド 2?
編集:Call メソッドを少し変更できる場合、解決策は何でしょうか?
解決
C# は AOP 指向の言語ではありません。いくつかの AOP 機能があり、他の機能をエミュレートすることもできますが、C# で AOP を作成するのは面倒です。
あなたがやりたいことを正確に実行する方法を探しましたが、それを実行する簡単な方法は見つかりませんでした。
私が理解しているように、これがあなたがやりたいことです:
[Log()]
public void Method1(String name, Int32 value);
そのためには 2 つの主なオプションがあります
MarshalByRefObject または ContextBoundObject からクラスを継承し、IMessageSink から継承する属性を定義します。 この記事 良い例があります。それでも、MarshalByRefObject を使用するとパフォーマンスが大幅に低下することを考慮する必要があります。つまり、パフォーマンスが 10 倍低下するということなので、試す前によく考えてください。
もう 1 つのオプションは、コードを直接挿入することです。つまり、実行時は、リフレクションを使用してすべてのクラスを「読み取り」、その属性を取得し、適切な呼び出しを挿入する必要があります (さらに言えば、Reflection.Emit メソッドは使用できないと思います。Reflection.Emit は使用できないと思います)。既存のメソッド内に新しいコードを挿入することはできません)。設計時には、これは CLR コンパイラの拡張機能を作成することを意味しますが、正直、それがどのように行われるかはわかりません。
最後のオプションは、 IoCフレームワーク. 。ほとんどの IoC フレームワークはメソッドのフックを許可するエントリ ポイントを定義することで機能するため、これは完璧な解決策ではないかもしれませんが、達成したい内容によっては、これがかなりの近似値になる可能性があります。
他のヒント
それを達成するための最も簡単な方法は、おそらく使用することです ポストシャープ. 。適用する属性に基づいてメソッド内にコードを挿入します。それはあなたが望むことを正確に行うことを可能にします。
別のオプションは、 プロファイリングAPI メソッド内にコードを挿入しますが、これは本当にハードコアです。
IDisposable インターフェイスを実装するクラス (Tracing と呼びます) を作成する場合、すべてのメソッド本体を
Using( Tracing tracing = new Tracing() ){ ... method body ...}
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 ...]
}
}
}
次の方法でそれを達成できます 傍受 などの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) { }
}
クラスの一部のメソッドのみをインターセプトする必要がある場合は、属性の処理が必要になることに注意してください。デフォルトでは、すべてのパブリック メソッドがインターセプトされます。
これを見てください - かなり重いものです..http://msdn.microsoft.com/en-us/magazine/cc164165.aspx
Essential .net - don box には、Interception と呼ばれる必要なものについての章がありました。ここに一部を抜粋しました(フォントの色については申し訳ありません。当時は暗いテーマを使用していました...)http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html
もっと簡単かもしれない別の方法を見つけました...
メソッドの宣言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 からの派生、またはリモート クラスやプロキシ クラスを使用するよりも軽量であることに同意していただけると幸いです。
まず、(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();
}
}
制限なくメソッドをトレースしたい場合 (コード適応、AOP フレームワーク、重複コードなし)、ちょっとした魔法が必要です。
真剣に、実行時に動作する AOP フレームワークを実装するために解決しました。
ここで見つけることができます: N懸念.NET AOPフレームワーク
このようなニーズに応えるために、この AOP フレームワークを作成することにしました。これは非常に軽量なシンプルなライブラリです。ロガーの例はホームページでご覧いただけます。
サードパーティのアセンブリを使用したくない場合は、コード ソース (オープン ソース) を参照し、両方のファイルをコピーできます。 アスペクトディレクトリ.cs そして Aspect.Directory.Entry.cs あなたの希望に合わせて調整します。これらのクラスを使用すると、実行時にメソッドを置き換えることができます。ただライセンスを尊重していただきたいと思います。
必要なもの、または最終的に AOP フレームワークを使用するよう説得するものが見つかることを願っています。
オープンソースフレームワークを利用できる Cインジェクト CodePlex で。最小限のコードを記述してインジェクターを作成し、CInject を使用してコードを迅速にインターセプトさせることができます。さらに、これはオープンソースであるため、これを拡張することもできます。
または、この記事で説明されている手順に従うこともできます。 IL を使用したメソッド呼び出しのインターセプト C# の Reflection.Emit クラスを使用して独自のインターセプターを作成します。
解決策はわかりませんが、私のアプローチは次のようになります。
クラス (またはそのメソッド) をカスタム属性で装飾します。プログラムの別の場所で、初期化関数にすべての型を反映させ、属性で修飾されたメソッドを読み取り、メソッドに IL コードを挿入します。実際にはもっと実用的かもしれません 交換する 呼び出すスタブによるメソッド LogStart
, 、実際の方法、そして LogEnd
. 。さらに、リフレクションを使用してメソッドを変更できるかどうかはわかりません。そのため、型全体を置き換える方が現実的かもしれません。
GOF デコレータ パターンを使用して、トレースが必要なすべてのクラスを「装飾」できる可能性があります。
おそらく実際に実用的なのは IOC コンテナを使用する場合のみです (ただし、前に指摘したように、IOC パスをたどる場合はメソッド インターセプトを検討した方がよいでしょう)。
Ayende に問い合わせて、彼がどうやってそれを行ったのかを尋ねる必要があります。http://ayende.com/Blog/archive/2009/11/19/can-you-hack-this-out.aspx
AOP はクリーンなコードを実装するために必須ですが、C# でブロックを囲みたい場合は、ジェネリック メソッドの方が比較的簡単に使用できます。(インテリジェンスと厳密に型指定されたコードを使用) 確かに、AOP の代替にはなり得ません。
それでも ポストシャープ バグの問題はほとんどありませんが (運用環境で使用する自信がありません)、良いものです。
汎用ラッパークラス、
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");
- 独自の AOP ライブラリを作成します。
- リフレクションを使用して、インスタンス上にロギング プロキシを生成します (既存のコードの一部を変更せずにそれを実行できるかどうかはわかりません)。
- アセンブリを書き換えて、ログ コードを挿入します (基本的に 1 と同じ)。
- CLR をホストし、このレベルでログを追加します (これは実装が最も難しい解決策だと思いますが、CLR に必要なフックがあるかどうかはわかりません)。
「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
}