粒紋呼叫篩選
粒紋呼叫篩選可提供攔截粒紋呼叫的方法。 篩選可以在粒紋呼叫前後執行程式碼。 您可以同時安裝多個篩選。 篩選為非同步進行,且可以修改 RequestContext、引數,以及所叫用方法的傳回值。 篩選也可以檢查在粒紋類別上所叫用方法的 MethodInfo,並可用來擲回或處理例外狀況。
粒紋呼叫篩選的一些範例用法如下:
- 授權:篩選可以檢查所叫用的方法,以及
RequestContext
中的引數或某些授權資訊,以判斷是否要允許呼叫繼續。 - 記錄/遙測:篩選可以記錄資訊及擷取計時資料,以及有關方法調用的其他統計資料。
- 錯誤處理:篩選可以攔截方法調用所擲回的例外狀況,並將其轉換成另一個例外狀況,或在通過篩選時處理例外狀況。
篩選分為兩種類別:
- 來電篩選
- 去電篩選
接收通話時,會執行來電篩選。 撥打電話時,會執行去電篩選。
來電篩選
傳入的粒紋呼叫篩選會實作 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()
的結果,才能執行下一個設定的篩選準則,最後執行粒紋方法本身。 在等候 Invoke()
方法之後,即可修改 Result
屬性。 ImplementationMethod
屬性傳回實作類別的 MethodInfo
。 介面方法的 MethodInfo
可以使用 InterfaceMethod
屬性進行存取。 針對粒紋的所有方法呼叫都會呼叫粒紋呼叫篩選,包括對粒紋延伸模組的呼叫 (IGrainExtension
實作),其會安裝在粒紋中。 例如,粒紋延伸模組會用來實作串流和取消權杖。 因此,應該預期 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);
}
在上述範例中,僅在 RequestContext
中將 "isAdmin"
設定為 true
時,才能呼叫 SpecialAdminOnlyOperation
方法。 如此一來,就可以使用粒紋呼叫篩選進行授權。 在此範例中,呼叫者必須負責確保 "isAdmin"
值已正確設定,且驗證已正確執行。 請注意,會在粒紋類別方法上指定 [AdminOnly]
屬性。 這是因為 ImplementationMethod
屬性會傳回實作的 MethodInfo
,而不是介面。 篩選也可以檢查 InterfaceMethod
屬性。
粒紋呼叫篩選排序
粒紋呼叫篩選會遵循定義的排序:
- 在相依性插入容器中設定的
IIncomingGrainCallFilter
實作,依據其所註冊的順序。 - 如果粒紋實作
IIncomingGrainCallFilter
,則為粒紋層級篩選。 - 粒紋方法實作或粒紋擴充方法實作。
每個 IIncomingGrainCallContext.Invoke()
的呼叫都會封裝下一個定義的篩選,讓每個篩選有機會在鏈結中的下一個篩選前後執行程式碼,最後執行粒紋方法本身。
去電篩選
傳出粒紋呼叫篩選類似於傳入粒紋呼叫篩選,主要差異在於其是在呼叫者 (用戶端) 叫用,而非在被呼叫者 (粒紋) 叫用。
傳出的粒紋呼叫篩選會實作 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()
的結果,才能執行下一個設定的篩選準則,最後執行粒紋方法本身。 在等候 Invoke()
方法之後,即可修改 Result
屬性。 所呼叫介面方法的 MethodInfo
可以使用 InterfaceMethod
屬性進行存取。 所有對粒紋的方法呼叫都會叫用傳出粒紋呼叫篩選,這包括對 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
可以是 ISiloHostBuilder 或 IClientBuilder 的執行個體。
同樣地,類別也可以註冊為傳出粒紋呼叫篩選。 以下是記錄每個粒紋方法結果的粒紋呼叫篩選範例:
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
可以是 ISiloHostBuilder 或 IClientBuilder 的執行個體。
使用案例
例外狀況轉換
當從伺服器擲回的例外狀況在用戶端上還原序列化時,您有時可能會收到下列例外狀況,而不是實際例外狀況:TypeLoadException: Could not find Whatever.dll.
如果包含例外狀況的組件無法供用戶端使用,就會發生這種情況。 例如,假設您在粒紋實作中使用 Entity Framework;則可能會擲回 EntityException
。 另一方面,用戶端不會 (且不應該) 參考 EntityFramework.dll
,因為其不知道基礎資料存取層。
當用戶端嘗試還原序列化 EntityException
時,其將會因為遺漏 DLL 而失敗;因此,會擲回 TypeLoadException,並隱藏原始的 EntityException
。
可能會有人認為這沒關係,因為用戶端永遠不會處理 EntityException
;否則,其必須參考 EntityFramework.dll
。
但是,如果用戶端至少需要記錄例外狀況,該怎麼辦? 問題在於原始錯誤訊息遺失。 解決此問題的其中一種方法,是攔截伺服器端例外狀況,並在用戶端上假設例外狀況類型未知時,以 Exception
類型的純文字例外狀況加以取代。
不過,我們必須記住一個重要事項:僅在呼叫端為粒紋用戶端時,才需要取代例外狀況。 如果呼叫端是另一個粒紋 (或是也會進行粒紋呼叫的 Orleans 基礎結構;例如,在 GrainBasedReminderTable
粒紋上) 我們也不需要取代例外狀況。
在伺服器端,這可以使用定址接收器層級攔截器來完成:
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();
}