Orleans 2.0 官方文档 —— 4.10.2 Grains -> 高级功能 -> 拦截器

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扩展用于实现Streams和Cancellation Tokens。因此,应该预料到ImplementationMethod的值并不总是grain类本身的方法。

配置呼入的grain调用过滤器

IIncomingGrainCallFilter的实现,可以通过依赖注入被注册成silo级的过滤器,也可以通过一个grain直接实现IIncomingGrainCallFilter,将其注册为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 log;

    public LoggingCallFilter(Factory<string, Logger> loggerFactory)
    {
        this.log = 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);
            this.log.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;
        }
    }
}

然后可以使用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);
}

在上面的例子中,只有当RequestContext中的"isAdmin"被设置为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的所有方法调用,包括对Orleans所提供的系统方法的调用,都会调用呼出grain调用过滤器。

配置呼出grain调用过滤器

IOutgoingGrainCallFilter的实现,可以使用依赖注入,在silo和客户端上进行注册。

委托可以注册为调用过滤器,如下所示:

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 log;

    public LoggingCallFilter(Factory<string, Logger> loggerFactory)
    {
        this.log = 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);
            this.log.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可以是ISiloHostBuiler或者IClientBuilder的实例。

使用场景

异常转换

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

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

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

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

但是如果客户端想要至少记录异常呢?问题是原始错误消息丢失。解决此问题的一种方法是拦截服务器端异常,如果异常类型在客户端可能是未知的,则将其替换为类型Exception的简单异常。

但是,我们必须记住一件重要的事情:如果调用者是grain客户端,我们只想替换异常。如果调用者是另一个grain(或者Orleans基础设施也在进行grain调用,例如在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)
{
  this.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 = this.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();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值