グレイン呼び出しフィルター

グレイン呼び出しフィルターは、グレイン呼び出しをインターセプトするための手段を提供します。 フィルターは、グレイン呼び出しの前後の両方でコードを実行できます。 複数のフィルターを同時にインストールできます。 フィルターは非同期であり、RequestContextと引数、および呼び出されるメソッドの戻り値を変更できます。 また、フィルターは、グレイン クラスに対して呼び出されるメソッドの MethodInfo を検査することもでき、例外のスローまたは処理に使用できます。

グレイン呼び出しフィルターのいくつかの使用例を次に示します。

  • 承認: フィルターは、呼び出されるメソッドと、RequestContext 内の引数または一部の承認情報を検査して、呼び出しの続行を許可するかどうかを決定できます。
  • ログまたはテレメトリ: フィルターは情報をログし、タイミング データや、メソッド呼び出しに関するその他の統計をキャプチャできます。
  • エラー処理: フィルターは、メソッド呼び出しによってスローされた例外をインターセプトし、それを別の例外に変換したり、例外がフィルターを通過するときに処理したりすることができます。

フィルターには、次の 2 種類があります。

  • 着信呼び出しフィルター
  • 発信呼び出しフィルター

着信呼び出しフィルターは、呼び出しを受信したときに実行されます。 発信呼び出しフィルターは、呼び出しを行ったときに実行されます。

着信呼び出しフィルター

着信グレイン呼び出しフィルターは、次の 1 つのメソッドを持つ IIncomingGrainCallFilter インターフェイスを実装します。

public interface IIncomingGrainCallFilter
{
    Task Invoke(IIncomingGrainCallContext context);
}

Invoke メソッドに渡される IIncomingGrainCallContext 引数の形状は次のとおりです。

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) メソッドは、次に構成されたフィルターを実行し、最終的にはグレイン メソッド自体を実行するために、IIncomingGrainCallContext.Invoke() の結果を待つか返す必要があります。 Invoke() メソッドの待機後、Result プロパティを変更できます。 ImplementationMethod プロパティは、実装クラスの MethodInfo を返します。 インターフェイス メソッドの MethodInfo には、InterfaceMethod プロパティを使用してアクセスできます。 グレイン呼び出しフィルターは、グレインへのすべてのメソッド呼び出しに対して呼び出されます。これには、グレインにインストールされているグレイン拡張機能 (IGrainExtension の実装) への呼び出しが含まれます。 たとえば、グレイン拡張機能は、ストリームとキャンセル トークンを実装するために使用されます。 このため、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 メソッドに対するすべての呼び出しは、7 の代わりに 38 を返します。

フィルターのもう 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 メソッドは、RequestContext"isAdmin"true に設定されている場合にのみ呼び出すことができます。 このように、グレイン呼び出しフィルターは承認に使用できます。 この例では、"isAdmin" 値が正しく設定され、認証が正しく実行されていることを保証するのは呼び出し元の責任です。 [AdminOnly] 属性は、グレイン クラス メソッドで指定されていることに注意してください。 これは、ImplementationMethod プロパティが、インターフェイスではなく実装の MethodInfo を返すためです。 フィルターは、InterfaceMethod プロパティをチェックすることもできます。

グレイン呼び出しフィルターの順序付け

グレイン呼び出しフィルターは、定義された順序に従います。

  1. 依存関係挿入コンテナーで構成された IIncomingGrainCallFilter 実装 (登録順)。
  2. グレインレベル フィルター (グレインが IIncomingGrainCallFilter を実装している場合)
  3. グレイン メソッドの実装またはグレイン拡張メソッドの実装。

IIncomingGrainCallContext.Invoke() を呼び出すたびに、次に定義されたフィルターがカプセル化されるため、各フィルターはチェーン内の次のフィルターの前後でコードを実行し、最終的にはグレイン メソッド自体を実行する機会が得られます。

発信呼び出しフィルター

発信呼び出しフィルターは、着信グレイン呼び出しフィルターと似ていますが、主な違いは、呼び出し先 (グレイン) ではなく呼び出し元 (クライアント) で呼び出されることです。

発信グレイン呼び出しフィルターは、次の 1 つのメソッドを持つ IOutgoingGrainCallFilter インターフェイスを実装します。

public interface IOutgoingGrainCallFilter
{
    Task Invoke(IOutgoingGrainCallContext context);
}

Invoke メソッドに渡される IOutgoingGrainCallContext 引数の形状は次のとおりです。

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) メソッドは、次に構成されたフィルターを実行し、最終的にはグレイン メソッド自体を実行するために、IOutgoingGrainCallContext.Invoke() の結果を待つか返す必要があります。 Invoke() メソッドの待機後、Result プロパティを変更できます。 呼び出されるインターフェイス メソッドの 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 が見つからないために失敗します。その結果、元の EntityException を非表示にして TypeLoadException がスローされます。

クライアントでは 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();
}