グレイン呼び出しフィルターは、グレイン呼び出しをインターセプトする方法を提供します。 フィルターは、グレイン呼び出しの前と後の両方でコードを実行できます。 複数のフィルターを同時にインストールできます。 フィルターは非同期であり、呼び出されるメソッドの RequestContext、引数、および戻り値を変更できます。 フィルターは、グレイン クラスで呼び出されるメソッドの MethodInfo を調べ、例外をスローまたは処理するために使用することもできます。
グレイン 呼び出しフィルターの使用例を次に示します。
-
承認: フィルターは、呼び出されるメソッドと、呼び出しの続行を許可するかどうかを判断するために、
RequestContext
内の引数または認証情報を検査できます。 - ログ/テレメトリ: フィルターを使用すると、情報をログに記録し、メソッド呼び出しに関するタイミング データやその他の統計情報をキャプチャできます。
- エラー処理: フィルターは、メソッド呼び出しによってスローされた例外をインターセプトし、他の例外に変換したり、フィルターを通過する例外を処理したりできます。
フィルターには、次の 2 種類があります。
- 着信通話フィルター
- 発信通話フィルター
着信呼び出しフィルターは、通話を受信するときに実行されます。 発信呼び出しフィルターは、呼び出しを行うときに実行されます。
着信通話フィルター
着信グレイン呼び出しフィルターは、次の 1 つのメソッドを持つ IIncomingGrainCallFilter インターフェイスを実装します。
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
IIncomingGrainCallContext メソッドに渡されるInvoke
引数の形状は次のとおりです。
public interface IIncomingGrainCallContext
{
/// <summary>
/// Gets the grain being invoked.
/// </summary>
IAddressable Grain { get; }
/// <summary>
/// Gets the <see cref="MethodInfo"/> for the interface method being invoked.
/// </summary>
MethodInfo InterfaceMethod { get; }
/// <summary>
/// Gets the <see cref="MethodInfo"/> for the implementation method being invoked.
/// </summary>
MethodInfo ImplementationMethod { get; }
/// <summary>
/// Gets the arguments for this method invocation.
/// </summary>
object[] Arguments { get; }
/// <summary>
/// Invokes the request.
/// </summary>
Task Invoke();
/// <summary>
/// Gets or sets the result.
/// </summary>
object Result { get; set; }
}
IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)
メソッドは、次に構成されたフィルターを実行し、最終的にはグレイン メソッド自体を実行するために、await
の結果をIIncomingGrainCallContext.Invoke()
または返す必要があります。
Result
メソッドを待機した後、Invoke()
プロパティを変更できます。
ImplementationMethod
プロパティは、実装クラスのMethodInfo
を返します。
MethodInfo
プロパティを使用して、インターフェイス メソッドのInterfaceMethod
にアクセスできます。 グレイン呼び出しフィルターは、グレインにインストールされているグレイン拡張機能 ( IGrainExtension
の実装) の呼び出しを含む、グレインへのすべてのメソッド呼び出しに対して呼び出されます。 たとえば、 Orleans では、グレイン拡張機能を使用してストリームとキャンセル トークンを実装します。 したがって、 ImplementationMethod
の値がグレイン クラス自体のメソッドであるとは限りません。
着信グレイン通話フィルターを設定する
IIncomingGrainCallFilter の実装は、依存関係の注入を使用してサイロ全体のフィルターとして登録するか、グレインが IIncomingGrainCallFilter
を直接実装することによってグレインレベルのフィルターとして登録することができます。
サイロ全体の穀粒呼び出しフィルター
次のように依存性注入を使用して、全体的なグレイン呼び出しフィルターとしてデリゲートを登録できます。
siloHostBuilder.AddIncomingGrainCallFilter(async context =>
{
// If the method being called is 'MyInterceptedMethod', then set a value
// on the RequestContext which can then be read by other filters or the grain.
if (string.Equals(
context.InterfaceMethod.Name,
nameof(IMyGrain.MyInterceptedMethod)))
{
RequestContext.Set(
"intercepted value", "this value was added by the filter");
}
await context.Invoke();
// If the grain method returned an int, set the result to double that value.
if (context.Result is int resultValue)
{
context.Result = resultValue * 2;
}
});
同様に、 AddIncomingGrainCallFilter ヘルパー メソッドを使用して、クラスをグレイン呼び出しフィルターとして登録できます。 すべてのグレイン メソッドの結果をログに記録するグレイン呼び出しフィルターの例を次に示します。
public class LoggingCallFilter : IIncomingGrainCallFilter
{
private readonly Logger _logger;
public LoggingCallFilter(Factory<string, Logger> loggerFactory)
{
_logger = loggerFactory(nameof(LoggingCallFilter));
}
public async Task Invoke(IIncomingGrainCallContext context)
{
try
{
await context.Invoke();
var msg = string.Format(
"{0}.{1}({2}) returned value {3}",
context.Grain.GetType(),
context.InterfaceMethod.Name,
string.Join(", ", context.Arguments),
context.Result);
_logger.Info(msg);
}
catch (Exception exception)
{
var msg = string.Format(
"{0}.{1}({2}) threw an exception: {3}",
context.Grain.GetType(),
context.InterfaceMethod.Name,
string.Join(", ", context.Arguments),
exception);
_logger.Info(msg);
// If this exception is not re-thrown, it is considered to be
// handled by this filter.
throw;
}
}
}
このフィルターは、 AddIncomingGrainCallFilter
拡張メソッドを使用して登録できます。
siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();
または、拡張メソッドなしでフィルターを登録できます。
siloHostBuilder.ConfigureServices(
services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());
グレインごとの呼び出しフィルター
グレイン クラスは、自身をグレイン呼び出しフィルターとして登録し、次のように IIncomingGrainCallFilter
実装することで、そのクラスに対して行われたすべての呼び出しをフィルター処理できます。
public class MyFilteredGrain
: Grain, IMyFilteredGrain, IIncomingGrainCallFilter
{
public async Task Invoke(IIncomingGrainCallContext context)
{
await context.Invoke();
// Change the result of the call from 7 to 38.
if (string.Equals(
context.InterfaceMethod.Name,
nameof(this.GetFavoriteNumber)))
{
context.Result = 38;
}
}
public Task<int> GetFavoriteNumber() => Task.FromResult(7);
}
前の例では、フィルターによって戻り値が変更されたため、GetFavoriteNumber
メソッドのすべての呼び出しは38
の代わりに7
を返します。
フィルターのもう 1 つのユース ケースは、次の例に示すように、アクセス制御です。
[AttributeUsage(AttributeTargets.Method)]
public class AdminOnlyAttribute : Attribute { }
public class MyAccessControlledGrain
: Grain, IMyFilteredGrain, IIncomingGrainCallFilter
{
public Task Invoke(IIncomingGrainCallContext context)
{
// Check access conditions.
var isAdminMethod =
context.ImplementationMethod.GetCustomAttribute<AdminOnlyAttribute>();
if (isAdminMethod && !(bool) RequestContext.Get("isAdmin"))
{
throw new AccessDeniedException(
$"Only admins can access {context.ImplementationMethod.Name}!");
}
return context.Invoke();
}
[AdminOnly]
public Task<int> SpecialAdminOnlyOperation() => Task.FromResult(7);
}
前の例では、SpecialAdminOnlyOperation
メソッドは、"isAdmin"
がtrue
でRequestContext
に設定されている場合にのみ呼び出すことができます。 この方法では、承認にグレイン呼び出しフィルターを使用できます。 この例では、 "isAdmin"
値が正しく設定され、認証が正しく実行されていることを確認するのは呼び出し元の責任です。
[AdminOnly]
属性はグレイン クラス メソッドで指定されることに注意してください。 これは、 ImplementationMethod
プロパティがインターフェイスではなく実装の MethodInfo
を返すからです。 フィルターでは、 InterfaceMethod
プロパティを確認することもできます。
グレイン呼び出しフィルターの順序付け
グレイン呼び出しフィルターは、定義された順序に従います。
-
IIncomingGrainCallFilter
依存関係挿入コンテナーで構成された実装は、それらが登録されている順序で行われます。 -
IIncomingGrainCallFilter
を実装している場合は、グレインレベルのフィルターを適用します。 - グレイン メソッドの実装またはグレイン拡張メソッドの実装。
IIncomingGrainCallContext.Invoke()
への各呼び出しは、次に定義されたフィルターをカプセル化します。これにより、各フィルターはチェーン内の次のフィルターの前後にコードを実行し、最終的にはグレイン メソッド自体を実行できます。
発信通話フィルター
発信グレインコールフィルターは、着信グレインコールフィルターに似ています。 主な違いは、呼び出し先 (グレイン) ではなく、呼び出し元 (クライアント) で呼び出される点です。
発信グレイン呼び出しフィルターは、次の 1 つのメソッドを持つ IOutgoingGrainCallFilter
インターフェイスを実装します。
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
IOutgoingGrainCallContext メソッドに渡されるInvoke
引数の形状は次のとおりです。
public interface IOutgoingGrainCallContext
{
/// <summary>
/// Gets the grain being invoked.
/// </summary>
IAddressable Grain { get; }
/// <summary>
/// Gets the <see cref="MethodInfo"/> for the interface method being invoked.
/// </summary>
MethodInfo InterfaceMethod { get; }
/// <summary>
/// Gets the arguments for this method invocation.
/// </summary>
object[] Arguments { get; }
/// <summary>
/// Invokes the request.
/// </summary>
Task Invoke();
/// <summary>
/// Gets or sets the result.
/// </summary>
object Result { get; set; }
}
IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)
メソッドは、次に構成されたフィルターを実行し、最終的にはグレイン メソッド自体を実行するために、await
の結果をIOutgoingGrainCallContext.Invoke()
または返す必要があります。
Result
メソッドを待機した後、Invoke()
プロパティを変更できます。
MethodInfo
プロパティを使用して、呼び出されるインターフェイス メソッドのInterfaceMethod
にアクセスできます。 出力グレイン呼び出しフィルターは、 Orleansによって行われたシステム メソッドの呼び出しを含め、グレインに対するすべてのメソッド呼び出しに対して呼び出されます。
発信グレイン呼び出しフィルターを構成する
依存関係の挿入を使用して、サイロとクライアントの両方に IOutgoingGrainCallFilter
の実装を登録できます。
次のような呼び出しフィルターとしてデリゲートを登録します。
builder.AddOutgoingGrainCallFilter(async context =>
{
// If the method being called is 'MyInterceptedMethod', then set a value
// on the RequestContext which can then be read by other filters or the grain.
if (string.Equals(
context.InterfaceMethod.Name,
nameof(IMyGrain.MyInterceptedMethod)))
{
RequestContext.Set(
"intercepted value", "this value was added by the filter");
}
await context.Invoke();
// If the grain method returned an int, set the result to double that value.
if (context.Result is int resultValue)
{
context.Result = resultValue * 2;
}
});
上記のコードでは、 builder
は、 ISiloHostBuilder または IClientBuilderのインスタンスである可能性があります。
同様に、クラスを外向きのグレインコールフィルターとして登録できます。 すべてのグレイン メソッドの結果をログに記録するグレイン呼び出しフィルターの例を次に示します。
public class LoggingCallFilter : IOutgoingGrainCallFilter
{
private readonly Logger _logger;
public LoggingCallFilter(Factory<string, Logger> loggerFactory)
{
_logger = loggerFactory(nameof(LoggingCallFilter));
}
public async Task Invoke(IOutgoingGrainCallContext context)
{
try
{
await context.Invoke();
var msg = string.Format(
"{0}.{1}({2}) returned value {3}",
context.Grain.GetType(),
context.InterfaceMethod.Name,
string.Join(", ", context.Arguments),
context.Result);
_logger.Info(msg);
}
catch (Exception exception)
{
var msg = string.Format(
"{0}.{1}({2}) threw an exception: {3}",
context.Grain.GetType(),
context.InterfaceMethod.Name,
string.Join(", ", context.Arguments),
exception);
this.log.Info(msg);
// If this exception is not re-thrown, it is considered to be
// handled by this filter.
throw;
}
}
}
このフィルターは、 AddOutgoingGrainCallFilter
拡張メソッドを使用して登録できます。
builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();
または、拡張メソッドなしでフィルターを登録できます。
builder.ConfigureServices(
services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());
デリゲート呼び出しフィルターの例と同様に、 builder
は、 ISiloHostBuilder または IClientBuilderのインスタンスである可能性があります。
活用事例
例外変換
サーバーからスローされた例外がクライアントで逆シリアル化されると、実際の例外ではなく、次の例外が発生することがあります。 TypeLoadException: Could not find Whatever.dll.
これは、例外を含むアセンブリがクライアントで使用できない場合に発生します。 たとえば、グレイン実装で Entity Framework を使用すると、エラー EntityException
がスローされる可能性があります。 一方、クライアントは基になるデータ アクセス層を認識していないため、 EntityFramework.dll
を参照しません (また、参照しないでください)。
クライアントが EntityException
を逆シリアル化しようとすると、DLL がないため失敗します。 その結果、 TypeLoadException がスローされ、元の EntityException
が非表示になります。
クライアントが EntityException
を処理しないため、これは許容されると主張する場合があります。それ以外の場合は、 EntityFramework.dll
を参照する必要があります。
ただし、クライアントが少なくとも例外をログに記録する必要がある場合はどうでしょうか。 問題は、元のエラー メッセージが失われることです。 この問題を回避する 1 つの方法は、サーバー側の例外をインターセプトし、クライアント側で例外の種類が不明な場合に Exception
型の単純な例外に置き換えます。
ただし、1 つの重要な点に注意してください。 呼び出し元がグレイン クライアントの場合にのみ、例外を置き換えます。 呼び出し元が別のグレイン (またはOrleans インフラストラクチャがグレイン呼び出しをGrainBasedReminderTable
グレインで行う場合) の場合は、例外を置き換えたくありません。
サーバー側では、サイロ レベルのインターセプターを使用してこれを行うことができます。
public class ExceptionConversionFilter : IIncomingGrainCallFilter
{
private static readonly HashSet<string> KnownExceptionTypeAssemblyNames =
new HashSet<string>
{
typeof(string).Assembly.GetName().Name,
"System",
"System.ComponentModel.Composition",
"System.ComponentModel.DataAnnotations",
"System.Configuration",
"System.Core",
"System.Data",
"System.Data.DataSetExtensions",
"System.Net.Http",
"System.Numerics",
"System.Runtime.Serialization",
"System.Security",
"System.Xml",
"System.Xml.Linq",
"MyCompany.Microservices.DataTransfer",
"MyCompany.Microservices.Interfaces",
"MyCompany.Microservices.ServiceLayer"
};
public async Task Invoke(IIncomingGrainCallContext context)
{
var isConversionEnabled =
RequestContext.Get("IsExceptionConversionEnabled") as bool? == true;
if (!isConversionEnabled)
{
// If exception conversion is not enabled, execute the call without interference.
await context.Invoke();
return;
}
RequestContext.Remove("IsExceptionConversionEnabled");
try
{
await context.Invoke();
}
catch (Exception exc)
{
var type = exc.GetType();
if (KnownExceptionTypeAssemblyNames.Contains(
type.Assembly.GetName().Name))
{
throw;
}
// Throw a base exception containing some exception details.
throw new Exception(
string.Format(
"Exception of non-public type '{0}' has been wrapped."
+ " Original message: <<<<----{1}{2}{3}---->>>>",
type.FullName,
Environment.NewLine,
exc,
Environment.NewLine));
}
}
}
その後、このフィルターをサイロに登録できます。
siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();
発信呼び出しフィルターを追加することで、クライアントによって行われた呼び出しのフィルターを有効にします。
clientBuilder.AddOutgoingGrainCallFilter(context =>
{
RequestContext.Set("IsExceptionConversionEnabled", true);
return context.Invoke();
});
これにより、クライアントは、例外変換を使用することをサーバーに通知します。
インターセプターからグレインを呼び出す
インターセプター クラスに IGrainFactory を挿入することで、インターセプターからグレイン呼び出しを行うことができます。
private readonly IGrainFactory _grainFactory;
public CustomCallFilter(IGrainFactory grainFactory)
{
_grainFactory = grainFactory;
}
public async Task Invoke(IIncomingGrainCallContext context)
{
// Hook calls to any grain other than ICustomFilterGrain implementations.
// This avoids potential infinite recursion when calling OnReceivedCall() below.
if (!(context.Grain is ICustomFilterGrain))
{
var filterGrain = _grainFactory.GetGrain<ICustomFilterGrain>(
context.Grain.GetPrimaryKeyLong());
// Perform some grain call here.
await filterGrain.OnReceivedCall();
}
// Continue invoking the call on the target grain.
await context.Invoke();
}
.NET