title: Grain调用拦截器

description: 本节介绍了Orleans中的调用拦截器及其相关应用

Grain调用过滤器提供了一种拦截Grain调用的方法。过滤器可以在Grain调用之前和之后执行代码。可以同时设置多个过滤器。过滤器是异步的,它可以修改RequestContext、参数和被调用方法的返回值。过滤器还可以检查被调用的方法的MethodInfo,可以用来抛出或处理异常。

Grain调用过滤器的一些使用示例:

  • 授权:过滤器可以检查被调用的方法和参数或RequestContext中的一些授权信息,以确定是否允许调用继续执行。
  • 日志/遥测:过滤器可以记录信息,捕捉时间数据和其他关于方法调用的统计数据。
  • 错误处理:过滤器可以拦截由方法调用抛出的异常,并将其转化为另一个异常或者在异常通过过滤器时进行处理。

过滤器有两种类型:

  • 传入调用过滤器
  • 传出调用过滤器

传入调用过滤器在接受调用时执行;传出调用过滤器在发起调用时执行。

传入调用过滤器

传入Grain调用过滤器应实现IIncomingGrainCallFilter接口,其包含一个方法:

  1. public interface IIncomingGrainCallFilter
  2. {
  3. Task Invoke(IIncomingGrainCallContext context);
  4. }

传递给Invoke方法的参数IIncomingGrainCallContext具体如下:

  1. public interface IIncomingGrainCallContext
  2. {
  3. /// <summary>
  4. /// Gets the grain being invoked.
  5. /// </summary>
  6. IAddressable Grain { get; }
  7. /// <summary>
  8. /// Gets the <see cref="MethodInfo"/> for the interface method being invoked.
  9. /// </summary>
  10. MethodInfo InterfaceMethod { get; }
  11. /// <summary>
  12. /// Gets the <see cref="MethodInfo"/> for the implementation method being invoked.
  13. /// </summary>
  14. MethodInfo ImplementationMethod { get; }
  15. /// <summary>
  16. /// Gets the arguments for this method invocation.
  17. /// </summary>
  18. object[] Arguments { get; }
  19. /// <summary>
  20. /// Invokes the request.
  21. /// </summary>
  22. Task Invoke();
  23. /// <summary>
  24. /// Gets or sets the result.
  25. /// </summary>
  26. object Result { get; set; }
  27. }

IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)方法必须等待或返回IIncomingGrainCallContext.Invoke()的结果,以执行下一个配置好的过滤器,最后执行Grain方法本身。在等待Invoke()方法后,Result属性可以被修改。ImplementationMethod属性返回实现类的MethodInfo。接口方法的MethodInfo可以使用InterfaceMethod属性来访问。Grain的调用过滤器对所有Grain方法的调用都生效,也包括Grain扩展(IGrainExtension的实现)的调用,这些扩展安装在Grain中。例如,Grain扩展被用来实现流和取消令牌。因此,应该假设ImplementationMethod的值不一定是是Grain类本身的一个方法。

配置传入调用过滤器

IIncomingGrainCallFilter的实现可以通过依赖注入注册为Silo级过滤器,也可以通过一个实现了IIncomingGrainCallFilter的Grain直接注册为Grain级过滤器。

Silo级Grain调用过滤器

一个委托可以像这样使用依赖注入注册为一个Silo级Grain调用过滤器:

  1. siloHostBuilder.AddIncomingGrainCallFilter(async context =>
  2. {
  3. // If the method being called is 'MyInterceptedMethod', then set a value
  4. // on the RequestContext which can then be read by other filters or the grain.
  5. if (string.Equals(context.InterfaceMethod.Name, nameof(IMyGrain.MyInterceptedMethod)))
  6. {
  7. RequestContext.Set("intercepted value", "this value was added by the filter");
  8. }
  9. await context.Invoke();
  10. // If the grain method returned an int, set the result to double that value.
  11. if (context.Result is int resultValue) context.Result = resultValue * 2;
  12. });

同样地,一个类可以使用AddIncomingGrainCallFilter方法注册为一个Grain调用过滤器。 下面是一个Grain调用过滤器的例子,它记录了每个Grain方法的结果:

  1. public class LoggingCallFilter : IIncomingGrainCallFilter
  2. {
  3. private readonly Logger log;
  4. public LoggingCallFilter(Factory<string, Logger> loggerFactory)
  5. {
  6. this.log = loggerFactory(nameof(LoggingCallFilter));
  7. }
  8. public async Task Invoke(IIncomingGrainCallContext context)
  9. {
  10. try
  11. {
  12. await context.Invoke();
  13. var msg = string.Format(
  14. "{0}.{1}({2}) returned value {3}",
  15. context.Grain.GetType(),
  16. context.InterfaceMethod.Name,
  17. string.Join(", ", context.Arguments),
  18. context.Result);
  19. this.log.Info(msg);
  20. }
  21. catch (Exception exception)
  22. {
  23. var msg = string.Format(
  24. "{0}.{1}({2}) threw an exception: {3}",
  25. context.Grain.GetType(),
  26. context.InterfaceMethod.Name,
  27. string.Join(", ", context.Arguments),
  28. exception);
  29. this.log.Info(msg);
  30. // If this exception is not re-thrown, it is considered to be
  31. // handled by this filter.
  32. throw;
  33. }
  34. }
  35. }

然后可以使用AddIncomingGrainCallFilter扩展方法来注册这个过滤器:

  1. siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

另外,也可以不使用扩展方法来注册过滤器:

  1. siloHostBuilder.ConfigureServices(
  2. services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());

逐Grain的Grain调用过滤器

一个Grain类可以将自己注册为Grain调用过滤器,并通过实现IIncomingGrainCallFilter来过滤对它的任何调用,就像这样:

  1. public class MyFilteredGrain : Grain, IMyFilteredGrain, IIncomingGrainCallFilter
  2. {
  3. public async Task Invoke(IIncomingGrainCallContext context)
  4. {
  5. await context.Invoke();
  6. // Change the result of the call from 7 to 38.
  7. if (string.Equals(context.InterfaceMethod.Name, nameof(this.GetFavoriteNumber)))
  8. {
  9. context.Result = 38;
  10. }
  11. }
  12. public Task<int> GetFavoriteNumber() => Task.FromResult(7);
  13. }

在上面的例子中,所有对GetFavoriteNumber方法的调用将返回38而不是7,因为返回值已经被过滤器修改了。

过滤器的另一个使用场景是访问控制,像下面这个例子:

  1. [AttributeUsage(AttributeTargets.Method)]
  2. public class AdminOnlyAttribute : Attribute { }
  3. public class MyAccessControlledGrain : Grain, IMyFilteredGrain, IIncomingGrainCallFilter
  4. {
  5. public Task Invoke(IIncomingGrainCallContext context)
  6. {
  7. // Check access conditions.
  8. var isAdminMethod = context.ImplementationMethod.GetCustomAttribute<AdminOnlyAttribute>();
  9. if (isAdminMethod && !(bool) RequestContext.Get("isAdmin"))
  10. {
  11. throw new AccessDeniedException($"Only admins can access {context.ImplementationMethod.Name}!");
  12. }
  13. return context.Invoke();
  14. }
  15. [AdminOnly]
  16. public Task<int> SpecialAdminOnlyOperation() => Task.FromResult(7);
  17. }

在上面的例子中,只有在RequestContext"isAdmin"被设置为true时,才能调用SpecialAdminOnlyOperation方法。通过这种方式,Grain调用过滤器可用于授权。在这个例子中,调用者负责确保"isAdmin"值被正确设置,并正确进行认证。请注意,[AdminOnly]特性是在Grain类方法上指定的。这是因为ImplementationMethod属性返回实现的MethodInfo,而不是接口的。过滤器也可以检查InterfaceMethod属性。

Grain调用过滤器的顺序

Grain调用过滤器遵循定义好的顺序:

  1. 在依赖注入容器中配置的IIncomingGrainCallFilter实现会按照它们被注册的顺序。
  2. 实现了IIncomingGrainCallFilter的Grain级过滤器。
  3. Grain方法的实现或Grain扩展方法的实现。

IIncomingGrainCallContext.Invoke()的每次调用都封装了下一个定义好的过滤器,这样每个过滤器都可以在过滤器链中的下一个过滤器前后执行代码,并最终执行Grain方法本身。

传出调用过滤器

传出Grain调用过滤器类似于传入Grain调用过滤器,主要区别在于它们是在调用者(客户端)而不是被调用者(Grain)上被调用。

传出Grain调用过滤器应实现IOutgoingGrainCallFilter接口,其包含一个方法:

  1. public interface IOutgoingGrainCallFilter
  2. {
  3. Task Invoke(IOutgoingGrainCallContext context);
  4. }

传递给Invoke方法的参数IOutgoingGrainCallContext具体如下:

  1. public interface IOutgoingGrainCallContext
  2. {
  3. /// <summary>
  4. /// Gets the grain being invoked.
  5. /// </summary>
  6. IAddressable Grain { get; }
  7. /// <summary>
  8. /// Gets the <see cref="MethodInfo"/> for the interface method being invoked.
  9. /// </summary>
  10. MethodInfo InterfaceMethod { get; }
  11. /// <summary>
  12. /// Gets the arguments for this method invocation.
  13. /// </summary>
  14. object[] Arguments { get; }
  15. /// <summary>
  16. /// Invokes the request.
  17. /// </summary>
  18. Task Invoke();
  19. /// <summary>
  20. /// Gets or sets the result.
  21. /// </summary>
  22. object Result { get; set; }
  23. }

IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)方法必须等待或返回IOutgoingGrainCallContext.Invoke()的结果,以执行下一个配置好的过滤器,最后执行Grain方法本身。在等待Invoke()方法后,Result属性可以被修改。ImplementationMethod属性返回实现类的MethodInfo。接口方法的MethodInfo可以使用InterfaceMethod属性来访问。传出Grain调用过滤器对调用Grain的方法都生效,也包括Orleans对系统方法的调用。

配置传出Grain调用过滤器

IOutgoingGrainCallFilter的实现可以通过依赖注入的方式在Silo以及客户端上注册。

一个委托可以像这样注册为一个调用过滤器:

  1. builder.AddOutgoingGrainCallFilter(async context =>
  2. {
  3. // If the method being called is 'MyInterceptedMethod', then set a value
  4. // on the RequestContext which can then be read by other filters or the grain.
  5. if (string.Equals(context.InterfaceMethod.Name, nameof(IMyGrain.MyInterceptedMethod)))
  6. {
  7. RequestContext.Set("intercepted value", "this value was added by the filter");
  8. }
  9. await context.Invoke();
  10. // If the grain method returned an int, set the result to double that value.
  11. if (context.Result is int resultValue) context.Result = resultValue * 2;
  12. });

在上述代码中,builder可以是ISiloHostBuilderIClientBuilder的实例。

同样地,一个类可以被注册为一个传出Grain调用过滤器。 下面是一个Grain调用过滤器的例子,它记录了每个Grain方法的结果:

  1. public class LoggingCallFilter : IOutgoingGrainCallFilter
  2. {
  3. private readonly Logger log;
  4. public LoggingCallFilter(Factory<string, Logger> loggerFactory)
  5. {
  6. this.log = loggerFactory(nameof(LoggingCallFilter));
  7. }
  8. public async Task Invoke(IOutgoingGrainCallContext context)
  9. {
  10. try
  11. {
  12. await context.Invoke();
  13. var msg = string.Format(
  14. "{0}.{1}({2}) returned value {3}",
  15. context.Grain.GetType(),
  16. context.InterfaceMethod.Name,
  17. string.Join(", ", context.Arguments),
  18. context.Result);
  19. this.log.Info(msg);
  20. }
  21. catch (Exception exception)
  22. {
  23. var msg = string.Format(
  24. "{0}.{1}({2}) threw an exception: {3}",
  25. context.Grain.GetType(),
  26. context.InterfaceMethod.Name,
  27. string.Join(", ", context.Arguments),
  28. exception);
  29. this.log.Info(msg);
  30. // If this exception is not re-thrown, it is considered to be
  31. // handled by this filter.
  32. throw;
  33. }
  34. }
  35. }

然后可以使用AddOutgoingGrainCallFilter扩展方法注册这个过滤器:

  1. builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

另外,也可以不使用扩展方法来注册过滤器:

  1. builder.ConfigureServices(
  2. services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());

与委托调用过滤器的例子一样,builder可以是ISiloHostBuilerIClientBuilder的一个实例。

使用案例

异常转换

当一个从服务器抛出的异常在客户端被反序列化时,有时你会得到后述的异常,而不是真正的那个异常:TypeLoadException: Could not find Whatever.dll.

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

当客户端试图反序列化EntityException时,由于缺少DLL,它无法反序列化,于是是抛出了一个隐藏了原始EntityExceptionTypeLoadException

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

但是,如果客户端仅仅是想记录这个异常怎么办?问题是,原始的错误信息丢失。解决这个问题的一个方法是拦截服务器端的异常,如果异常类型对于客户端可能是未知的,就用Exception类型的普通异常来代替。

然而,有一件重要的事情我们必须记住:如果调用者是Grain客户端,我们只想替换一个异常;如果调用者是另一个Grain(或同样在进行Grain调用的Orleans基础设施,例如,在GrainBasedReminderTable Grain上),我们不希望替换异常。

在服务器端,这可以通过一个Silo级拦截器来完成:

  1. public class ExceptionConversionFilter : IIncomingGrainCallFilter
  2. {
  3. private static readonly HashSet<string> KnownExceptionTypeAssemblyNames =
  4. new HashSet<string>
  5. {
  6. typeof(string).Assembly.GetName().Name,
  7. "System",
  8. "System.ComponentModel.Composition",
  9. "System.ComponentModel.DataAnnotations",
  10. "System.Configuration",
  11. "System.Core",
  12. "System.Data",
  13. "System.Data.DataSetExtensions",
  14. "System.Net.Http",
  15. "System.Numerics",
  16. "System.Runtime.Serialization",
  17. "System.Security",
  18. "System.Xml",
  19. "System.Xml.Linq",
  20. "MyCompany.Microservices.DataTransfer",
  21. "MyCompany.Microservices.Interfaces",
  22. "MyCompany.Microservices.ServiceLayer"
  23. };
  24. public async Task Invoke(IIncomingGrainCallContext context)
  25. {
  26. var isConversionEnabled =
  27. RequestContext.Get("IsExceptionConversionEnabled") as bool? == true;
  28. if (!isConversionEnabled)
  29. {
  30. // If exception conversion is not enabled, execute the call without interference.
  31. await context.Invoke();
  32. return;
  33. }
  34. RequestContext.Remove("IsExceptionConversionEnabled");
  35. try
  36. {
  37. await context.Invoke();
  38. }
  39. catch (Exception exc)
  40. {
  41. var type = exc.GetType();
  42. if (KnownExceptionTypeAssemblyNames.Contains(type.Assembly.GetName().Name))
  43. {
  44. throw;
  45. }
  46. // Throw a base exception containing some exception details.
  47. throw new Exception(
  48. string.Format(
  49. "Exception of non-public type '{0}' has been wrapped."
  50. + " Original message: <<<<----{1}{2}{3}---->>>>",
  51. type.FullName,
  52. Environment.NewLine,
  53. exc,
  54. Environment.NewLine));
  55. }
  56. }
  57. }

这个拦截器可以在Silo上注册:

  1. siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

通过添加一个传出调用过滤器,对客户端的调用启用过滤器:

  1. clientBuilder.AddOutgoingGrainCallFilter(context =>
  2. {
  3. RequestContext.Set("IsExceptionConversionEnabled", true);
  4. return context.Invoke();
  5. });

这样,客户端告诉服务器,它想使用异常转换。

在拦截器中调用Grain

通过在拦截器类中注入IGrainFactory,可以在拦截器中进行Grain调用:

  1. private readonly IGrainFactory grainFactory;
  2. public CustomCallFilter(IGrainFactory grainFactory)
  3. {
  4. this.grainFactory = grainFactory;
  5. }
  6. public async Task Invoke(IIncomingGrainCallContext context)
  7. {
  8. // Hook calls to any grain other than ICustomFilterGrain implementations.
  9. // This avoids potential infinite recursion when calling OnReceivedCall() below.
  10. if (!(context.Grain is ICustomFilterGrain))
  11. {
  12. var filterGrain = this.grainFactory.GetGrain<ICustomFilterGrain>(context.Grain.GetPrimaryKeyLong());
  13. // Perform some grain call here.
  14. await filterGrain.OnReceivedCall();
  15. }
  16. // Continue invoking the call on the target grain.
  17. await context.Invoke();
  18. }