grain调用过滤器提供了一种拦截grain调用的方法。过滤器可以在grain调用之前和之后执行代码。可以同时安装多个过滤器。过滤器是异步的,它可以修改RequestContext
,参数和被调用方法的返回值。它也可以检查正在grain类上被调用的方法的MethodInfo
,还可以用于抛出或处理异常。
以下是grain调用过滤器的一些示例用法:
RequestContext
中的某些授权信息,以确定是否允许调用继续进行。过滤器有两种形式:
接收调用时,执行呼入调用过滤器。发出调用时,执行呼出调用过滤器。
呼入的grain调用过滤器实现了IIncomingGrainCallFilter
接口,它有一个方法:
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
传递给Invoke
方法的IIncomingGrainCallContext
参数,具有以下形式:
public interface IIncomingGrainCallContext
{
///
/// Gets the grain being invoked.
///
IAddressable Grain { get; }
///
/// Gets the for the interface method being invoked.
///
MethodInfo InterfaceMethod { get; }
///
/// Gets the for the implementation method being invoked.
///
MethodInfo ImplementationMethod { get; }
///
/// Gets the arguments for this method invocation.
///
object[] Arguments { get; }
///
/// Invokes the request.
///
Task Invoke();
///
/// Gets or sets the result.
///
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类本身的方法。
IIncomingGrainCallFilter
的实现,可以通过依赖注入被注册成silo级的过滤器,也可以通过一个grain直接实现IIncomingGrainCallFilter
,将其注册为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();
或者,不用扩展方法,也可以注册此过滤器:
siloHostBuilder.ConfigureServices(
services => services.AddSingleton());
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();
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调用过滤器遵循已定义的顺序:
IIncomingGrainCallFilter
的实现,以它们被注册的顺序,配置在依赖注入容器中。IIncomingGrainCallFilter
。对IIncomingGrainCallContext.Invoke()
的每个调用,都会封装下一个定义的过滤器,以便每个过滤器都有机会在链中的下一个过滤器之前和之后执行代码,最终执行grain方法本身。
呼出的grain调用过滤器类似于呼入的grain调用过滤器,主要区别在于,它们在调用者(客户端)而不是被调用者(grain)上调用。
呼出的grain调用过滤器实现了IOutgoingGrainCallFilter
接口,它有一个方法:
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
传递给该Invoke
方法的IOutgoingGrainCallContext
参数,具有以下形式:
public interface IOutgoingGrainCallContext
{
///
/// Gets the grain being invoked.
///
IAddressable Grain { get; }
///
/// Gets the for the interface method being invoked.
///
MethodInfo InterfaceMethod { get; }
///
/// Gets the arguments for this method invocation.
///
object[] Arguments { get; }
///
/// Invokes the request.
///
Task Invoke();
///
/// Gets or sets the result.
///
object Result { get; set; }
}
IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)
方法必须等待或返回IOutgoingGrainCallContext.Invoke()
的结果,才能执行下一个已配置的过滤器,并最终执行grain方法本身。在等待该Invoke()
方法后,可以修改Result
属性。可以使用InterfaceMethod
属性,来访问接口方法的MethodInfo
。对一个grain的所有方法调用,包括对Orleans所提供的系统方法的调用,都会调用呼出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
可以是ISiloHostBuilder
或IClientBuilder
的一个实例。
类似地,可以将类注册为呼出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();
或者,不用扩展方法,也可以注册此过滤器:
builder.ConfigureServices(
services => services.AddSingleton());
与委托调用过滤器示例一样,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();
通过添加呼出调用过滤器,为客户端进行的调用,启用此过滤器:
clientBuilder.AddOutgoingGrainCallFilter(context =>
{
RequestContext.Set("IsExceptionConversionEnabled", true);
return context.Invoke();
});
这样客户端告诉服务器,它想要使用异常转换。
通过向拦截器类中注入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(context.Grain.GetPrimaryKeyLong());
// Perform some grain call here.
await filterGrain.OnReceivedCall();
}
// Continue invoking the call on the target grain.
await context.Invoke();
}