.NETで動的プロキシを作成する方法の一つに DispatchProxy があります。
これを使う事でAOP的な処理を実現できますが、非同期処理の前後に非同期処理を差し込みたい場合にどうするのか気になったので実装してみました。
はじめに
DispatchProxyは指定したインターフェースのメソッド呼び出しをInvokeメソッド1へとバイパスする動的プロキシを作成します。
例えば、このような使い方が可能です。
using System.Reflection;
// 動的プロキシの作成
var svc = DispatchProxy.Create<ICheckService, SimpleProxy>();
var r = svc.FirstCheck("check1");
Console.WriteLine(r);
svc.SecondCheck("check2", 123);
// インターフェース定義
interface ICheckService
{
string FirstCheck(string a);
void SecondCheck(string a, int b);
}
// DispatchProxyサブクラス
class SimpleProxy : DispatchProxy
{
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
Console.WriteLine($"* Invoke: method={targetMethod}, args={string.Join(',', args ?? [])}");
return "ok";
}
}
実行結果はこうなります。
$ dotnet run
* Invoke: method=System.String FirstCheck(System.String), args=check1
ok
* Invoke: method=Void SecondCheck(System.String, Int32), args=check2,123
Invoke の結果として ok という文字列を返していますが、void のように InvalidCastException の発生しない戻り値の型であれば正常に実行できます。
非同期処理の前後に非同期処理
それでは本題です。
ここでは次の要件を実現します。
- 戻り値が
TaskもしくはTask<T>の場合、その前後に非同期処理((MethodInfo, object?[]?) -> Task)を差し込む - それ以外は何もしない(通常のメソッド実行のみ)
具体的には、実行対象の戻り値の型が Task か Task<T> の場合に次のような処理を実施します。2
await 前処理;
await targetMethod.Invoke(・・・);
await 後処理;
これを実現するDispatchProxyのサブクラスは、とりあえずこのようになりました。
using System.Reflection;
using AsyncFunc = System.Func<(System.Reflection.MethodInfo targetMethod, object?[]? args), System.Threading.Tasks.Task>;
public class AsyncProxy<T> : DispatchProxy
{
private T? _target;
private AsyncFunc? _beforeTask;
private AsyncFunc? _afterTask;
// 動的プロキシの作成
public static T CreateProxy(T target, AsyncFunc? beforeTask = null, AsyncFunc? afterTask = null)
{
var proxy = Create<T, AsyncProxy<T>>();
if (proxy is AsyncProxy<T> p)
{
p._target = target;
p._beforeTask = beforeTask;
p._afterTask = afterTask;
}
return proxy;
}
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
var returnType = targetMethod?.ReturnType;
if (returnType != null)
{
// 戻り値がTaskの場合
if (returnType == typeof(Task))
{
return InterceptAsync(targetMethod!, args);
}
// 戻り値がTask<R>の場合
else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
var genericType = returnType.GetGenericArguments()[0];
var method = typeof(AsyncProxy<T>).GetMethod("InterceptGenericAsync", BindingFlags.NonPublic | BindingFlags.Instance);
// 具体型を割り当てた MethodInfo の作成
var genericMethod = method!.MakeGenericMethod(genericType);
return genericMethod.Invoke(this, new object?[]{targetMethod!, args});
}
}
// その他
return targetMethod?.Invoke(_target, args);
}
private async Task InterceptAsync(MethodInfo targetMethod, object?[]? args)
{
if (_beforeTask != null)
{
await _beforeTask((targetMethod, args));
}
var r = targetMethod.Invoke(_target, args)!;
if (r is Task task)
{
await task;
}
if (_afterTask != null)
{
await _afterTask((targetMethod, args));
}
}
private async Task<R> InterceptGenericAsync<R>(MethodInfo targetMethod, object?[]? args)
{
if (_beforeTask != null)
{
await _beforeTask((targetMethod, args));
}
var res = await (Task<R>)targetMethod.Invoke(_target, args)!;
if (_afterTask != null)
{
await _afterTask((targetMethod, args));
}
return res;
}
}
戻り値が Task の場合は単純に非同期メソッド(上記InterceptAsync)を呼び出して処理するだけですが、ジェネリック型を使った Task<R> の場合は型引数 R をどう解決するかが課題となります。
スマートな解決策を思いつかなかったので、リフレクションを使う事にしました。
具体的には、MakeGenericMethod で InterceptGenericAsync の型引数へ具体型を割り当て Invoke します。
ちなみに、GetType().GetMethod("InterceptGenericAsync", ・・・) でメソッドを取得すると戻り値が null(該当メソッドが見つからない)となるので、上記では typeof(AsyncProxy<T>).GetMethod("InterceptGenericAsync", ・・・) としています。
動作確認
下記コードで動作確認してみます。
var proxy = AsyncProxy<ICheckService>.CreateProxy(
new CheckService(),
async (x) =>
{
var (_, a) = x;
Console.WriteLine($"* before start: {string.Join(",", a ?? [])}");
await Task.Delay(100);
Console.WriteLine($"* before end: {string.Join(",", a ?? [])}");
},
async (x) =>
{
var (_, a) = x;
Console.WriteLine($"* after start: {string.Join(",", a ?? [])}");
await Task.Delay(100);
Console.WriteLine($"* after end: {string.Join(",", a ?? [])}");
}
);
proxy.FirstCheck("a1");
Console.WriteLine("-----");
await proxy.SecondCheckAsync("b2", 2);
Console.WriteLine("-----");
var res = await proxy.ThirdCheckAsync("c3", 3);
Console.WriteLine(res);
Console.WriteLine("-----");
try
{
await proxy.FourthCheckAsync("d4", false);
}
catch (Exception ex)
{
Console.WriteLine($"** ERROR: {ex.Message}");
}
Console.WriteLine("-----");
try
{
var _ = await proxy.FifthCheckAsync("e5", false);
}
catch (Exception ex)
{
Console.WriteLine($"** ERROR: {ex.Message}");
}
interface ICheckService
{
int FirstCheck(string id);
Task SecondCheckAsync(string id, int value);
Task<string> ThirdCheckAsync(string id, int value);
Task FourthCheckAsync(string id, bool value);
Task<string> FifthCheckAsync(string id, bool value);
}
class CheckService : ICheckService
{
public int FirstCheck(string id)
{
Console.WriteLine($"FirstCheck: {id}");
return id.Length;
}
public async Task SecondCheckAsync(string id, int value)
{
Console.WriteLine($"SecondCheckAsync start: {id}, {value}");
await Task.Delay(50);
Console.WriteLine($"SecondCheckAsync end: {id}, {value}");
}
public async Task<string> ThirdCheckAsync(string id, int value)
{
Console.WriteLine($"ThirdCheckAsync start: {id}, {value}");
await Task.Delay(50);
var res = $"ok-{id}-{value}";
Console.WriteLine($"ThirdCheckAsync end: {id}, {value}");
return res;
}
public async Task FourthCheckAsync(string id, bool value)
{
Console.WriteLine($"FourthCheckAsync start: {id}, {value}");
await Task.Delay(50);
if (!value)
{
throw new Exception("FourthCheckAsync Error");
}
Console.WriteLine($"FourthCheckAsync end: {id}, {value}");
}
public async Task<string> FifthCheckAsync(string id, bool value)
{
Console.WriteLine($"FifthCheckAsync start: {id}, {value}");
await Task.Delay(50);
if (!value)
{
throw new Exception("FifthCheckAsync Error");
}
Console.WriteLine($"FifthCheckAsync end: {id}, {value}");
return "ok";
}
}
実行結果はこのようになり、とりあえずは意図したように動作しています。
$ dotnet run
FirstCheck: a1
-----
* before start: b2,2
* before end: b2,2
SecondCheckAsync start: b2, 2
SecondCheckAsync end: b2, 2
* after start: b2,2
* after end: b2,2
-----
* before start: c3,3
* before end: c3,3
ThirdCheckAsync start: c3, 3
ThirdCheckAsync end: c3, 3
* after start: c3,3
* after end: c3,3
ok-c3-3
-----
* before start: d4,False
* before end: d4,False
FourthCheckAsync start: d4, False
** ERROR: FourthCheckAsync Error
-----
* before start: e5,False
* before end: e5,False
FifthCheckAsync start: e5, False
** ERROR: FifthCheckAsync Error