Grain 调用筛选器

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

grain 调用筛选器的一些示例用法如下:

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

筛选器有两种风格:

  • 传入调用筛选器
  • 传出调用筛选器

接收调用时执行传入调用筛选器。 发出调用时执行传出调用筛选器。

传入调用筛选器

传入的 grain 调用筛选器实现 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() 的结果,才能执行配置的下一个筛选器并最终执行 grain 方法本身。 等待 Invoke() 方法后可以修改 Result 属性。 ImplementationMethod 属性返回实现类的 MethodInfo。 可以使用 InterfaceMethod 属性访问接口方法的 MethodInfo。 对 grain 的所有方法调用都会调用 grain 调用筛选器,这包括对安装在 grain 中的 grain 扩展(IGrainExtension 的实现)的调用。 例如,grain 扩展用于实现流和取消标记。 因此,应该预料到 ImplementationMethod 的值并非始终是 grain 类本身的方法。

配置传入的 grain 调用筛选器

IIncomingGrainCallFilter 的实现可以通过依赖项注入注册为 silo 范围的筛选器,也可以直接通过实现 IIncomingGrainCallFilter 的 grain 注册为 grain 级别的筛选器。

Silo 范围的 grain 调用筛选器

可以使用依赖项注入将委托注册为 silo 范围的 grain 调用筛选器,如下所示:

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 帮助器方法将类注册为 grain 调用筛选器。 下面是一个 grain 调用筛选器的示例,它记录每个 grain 方法的结果:

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

基于 grain 的 grain 调用筛选器

grain 类可将自身注册为 grain 调用筛选器,并通过实现 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"RequestContext 中设置为 true 时才能调用 SpecialAdminOnlyOperation 方法。 这样,便可将 grain 调用筛选器用于授权。 在此示例中,调用方负责确保正确设置 "isAdmin" 值并正确执行身份验证。 请注意,[AdminOnly] 属性是在 grain 类方法中指定的。 这是因为,ImplementationMethod 属性返回实现的 MethodInfo 而不是接口。 筛选器还可以检查 InterfaceMethod 属性。

grain 调用筛选器顺序

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

  1. 在依赖项注入容器中配置的 IIncomingGrainCallFilter 实现,遵循其注册顺序。
  2. grain 级别的筛选器(如果 grain 实现 IIncomingGrainCallFilter)。
  3. grain 方法实现或 grain 扩展方法实现。

IIncomingGrainCallContext.Invoke() 的每次调用都会封装定义的下一个筛选器,因此每个筛选器都有机会在链中的下一个筛选器之前和之后执行代码,并最终执行 grain 方法本身。

传出调用筛选器

传出的 grain 调用筛选器类似于传入的 grain 调用筛选器,主要差别在于,它们是在调用方(客户端)而不是被调用方 (grain) 上调用的。

传出的 grain 调用筛选器实现 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() 的结果,才能执行配置的下一个筛选器并最终执行 grain 方法本身。 等待 Invoke() 方法后可以修改 Result 属性。 可以使用 InterfaceMethod 属性访问被调用的接口方法的 MethodInfo。 对 grain 的所有方法调用都会调用传出的 grain 调用筛选器,这包括对 Orleans 发出的系统方法调用。

配置传出的 grain 调用筛选器

可以使用依赖项注入在 silo 和客户端上注册 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 的实例。

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

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.

如果包含异常的程序集对客户端不可用,则会发生这种情况。 例如,假设你在 grain 实现中使用实体框架,那么,可能会引发 EntityException。 另一方面,客户端不会(也不应该)引用 EntityFramework.dll,因为它不知道基础数据访问层。

当客户端尝试反序列化 EntityException 时,它会由于缺少 DLL 而失败;因此,会引发 TypeLoadException 并隐藏原始 EntityException

有人可能会争辩说这很好,因为客户端永远不会处理 EntityException;否则,它必须引用 EntityFramework.dll

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

但是,我们必须记住一个要点:仅当调用方是 grain 客户端时才需要替换异常。 如果调用方是另一个 grain(或者是也在发出 grain 调用的 Orleans 基础结构;例如在 GrainBasedReminderTable grain 上),则不需要替换异常。

在服务器端,可以通过 silo 级别的拦截器来做到一点:

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

然后可以在 silo 上注册此筛选器:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

通过添加传出调用筛选器针对客户端发出的调用启用筛选器:

clientBuilder.AddOutgoingGrainCallFilter(context =>
{
    RequestContext.Set("IsExceptionConversionEnabled", true);
    return context.Invoke();
});

这样,客户端就会告知服务器它想要使用异常转换。

从拦截器调用 grain

可以通过将 IGrainFactory 注入拦截器类来从拦截器发出 grain 调用:

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