Grain 调用筛选器

粒度调用筛选器提供了拦截粒度调用的方法。 筛选器可以在粒度调用前后执行代码。 可以同时安装多个筛选器。 筛选器是异步的,可以修改 RequestContext、参数和要调用的方法的返回值。 筛选器还可以检查 MethodInfo 在粒度类上调用的方法,并可用于引发或处理异常。

一些“粒度呼叫过滤器”的示例用法如下:

  • 授权:筛选器可以检查调用的方法以及参数或授权信息 RequestContext,以确定是否允许调用继续。
  • 日志记录/遥测:筛选器可以记录信息和捕获计时数据以及有关方法调用的其他统计信息。
  • 错误处理:筛选器可以截获方法调用引发的异常,并将它们转换为其他异常,或者在它们通过筛选器时处理异常。

筛选器分为两种类型:

  • 传入呼叫筛选器
  • 外拨电话过滤器

接收呼叫时,将执行传入呼叫筛选器。 发出呼叫时,将执行传出呼叫筛选器。

传入呼叫筛选器

传入粒度调用筛选器实现 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()的结果以执行下一个配置的筛选器,并最终执行粒度方法本身。 可以在等待Invoke()方法后修改Result属性。 该 ImplementationMethod 属性返回 MethodInfo 实现类。 可以通过InterfaceMethod属性访问MethodInfo接口方法。 对粒度的所有方法调用都调用粒度调用筛选器,包括对粒度中安装的粒度扩展(实现 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 因为筛选器更改了返回值。

筛选器的另一个用例是访问控制,如以下示例所示:

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

在前面的示例中,只有当 "isAdmin" 设置为 true 时,才能调用 SpecialAdminOnlyOperation 方法。 通过这种方式,您可以使用粒度调用筛选器来进行授权。 在此示例中,调用方负责确保 "isAdmin" 正确设置值并正确执行身份验证。 请注意,特性 [AdminOnly] 是在粒度类方法上指定的。 这是因为属性 ImplementationMethod 返回的是实现的 MethodInfo,而不是接口的 MethodInfo。 筛选器还可以检查 InterfaceMethod 属性。

粒度调用筛选器排序

粒度调用筛选器遵循定义的顺序:

  1. IIncomingGrainCallFilter 按照它们在依赖项注入容器中注册的顺序进行配置的实现。
  2. 颗粒级筛选器(如果颗粒实现 IIncomingGrainCallFilter)。
  3. 粒度方法的实现或颗粒扩展方法的实现。

每次对 IIncomingGrainCallContext.Invoke() 的调用都会包裹下一个定义的筛选器,使每个筛选器有机会在链中的下一个筛选器执行代码的前后运行,并最终执行 grain 方法本身。

外拨电话过滤器

传出粒度调用筛选器类似于传入的粒度调用筛选器。 主要区别在于它们是在调用方(客户端)而不是被调用方(粒子)上执行的。

传出粒度调用筛选器实现 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() 的结果,以执行下一个配置的筛选器,并最终调用 Grain 方法本身。 可以在等待Invoke()方法后修改Result属性。 可以通过 InterfaceMethod 属性访问正在调用的接口方法 MethodInfo。 对于对粒度的所有方法调用(包括对系统方法的 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 可以是 ISiloHostBuilderIClientBuilder.

同样,可以将类注册为传出粒度调用筛选器。 下面是记录每个粒度方法结果的粒度调用筛选器的示例:

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 可以是 ISiloHostBuilderIClientBuilder 的一个实例。

用例

异常转换

在客户端上反序列化服务器引发的异常时,有时可能会得到以下异常,而不是实际异常: TypeLoadException: Could not find Whatever.dll.

如果包含异常的程序集对客户端不可用,将发生这种情况。 例如,假设在粒度实现中使用 Entity Framework;可能会抛出一个 EntityException。 另一方面,客户端不(也不应)引用 EntityFramework.dll ,因为它不知道基础数据访问层。

当客户端尝试反序列化 EntityException 时,由于缺少 DLL 而失败。 因此,一个 TypeLoadException 被抛出,掩盖原来的 EntityException

人们可能会认为这是可以接受的,因为客户端永远不会处理EntityException;否则,它将需要引用EntityFramework.dll

但是,如果客户端至少想要记录异常,该怎么办? 问题是原始错误消息丢失。 解决此问题的一种方法是在假定客户端异常类型未知的情况下,截获服务器端异常,并将其替换为类型为 Exception 的普通异常。

但是,请记住一个重要事项: 如果调用方是粒度客户端,则只想替换异常。 如果调用方是另一个grain(或者是发出grain调用的OrleansGrainBasedReminderTable基础结构),则不希望替换异常。

在服务器端,可以使用组件级拦截器执行此操作。

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