Filtry volání agregace
Filtry volání agregace poskytují prostředky pro zachycování volání odstupňovaného intervalu. Filtry můžou spouštět kód před i po volání agregace. Současně lze nainstalovat více filtrů. Filtry jsou asynchronní a mohou upravovat RequestContext, argumenty a návratová hodnota metody, která je vyvolána. Filtry můžou také zkontrolovat MethodInfo metodu vyvolanou ve třídě agregace a lze ji použít k vyvolání nebo zpracování výjimek.
Mezi příklady použití filtrů volání agregace patří:
- Autorizace: Filtr může zkontrolovat vyvolánou metodu a argumenty nebo některé informace o autorizaci,
RequestContext
abyste zjistili, jestli má volání pokračovat nebo ne. - Protokolování/telemetrie: Filtr může protokolovat informace a zaznamenávat data časování a další statistiky o vyvolání metody.
- Zpracování chyb: Filtr může zachytit výjimky vyvolané metodou a transformovat ji na jinou výjimku nebo zpracovat výjimku při průchodu filtrem.
Filtry mají dvě varianty:
- Filtry příchozích hovorů
- Filtry odchozích hovorů
Filtry příchozích volání se provádějí při přijetí hovoru. Filtry odchozích volání se provádějí při volání.
Filtry příchozích hovorů
Filtry příchozích volání agregace implementují rozhraní, které má jednu metodu IIncomingGrainCallFilter :
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
Argument IIncomingGrainCallContext předaný Invoke
metodě má následující tvar:
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; }
}
Metoda IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)
musí očekávat nebo vrátit výsledek IIncomingGrainCallContext.Invoke()
spuštění dalšího nakonfigurovaného filtru a nakonec samotnou metodu agregace. Vlastnost Result
lze upravit po čekání na metodu Invoke()
. Vlastnost ImplementationMethod
vrátí MethodInfo
třídu implementace. K MethodInfo
metodě rozhraní lze přistupovat pomocí InterfaceMethod
vlastnosti. Filtry volání agregace se volají pro všechna volání metod agregace a to zahrnuje volání rozšíření odstupňovaného intervalu IGrainExtension
(implementace ), která jsou nainstalována v agregačním intervalu. Rozšíření agregací se například používají k implementaci Toky a tokenů zrušení. Proto by se mělo očekávat, že hodnota ImplementationMethod
není vždy metodou v samotné třídě zrnitosti.
Konfigurace filtrů příchozích volání agregace
Implementace můžou být buď registrovány jako filtry IIncomingGrainCallFilter pro silo prostřednictvím injektáže závislostí, nebo je možné je zaregistrovat jako filtry na úrovni podrobností prostřednictvím přímo implementovaných IIncomingGrainCallFilter
filtrů.
Filtry volání na úrovni Silo
Delegáta je možné zaregistrovat jako filtr volání pro silo-wide pomocí injektáže závislostí, například takto:
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;
}
});
Podobně lze třídu zaregistrovat jako filtr volání agregace pomocí AddIncomingGrainCallFilter pomocné metody. Tady je příklad filtru volání agregace, který protokoluje výsledky každé metody agregace:
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;
}
}
}
Tento filtr pak můžete zaregistrovat pomocí AddIncomingGrainCallFilter
metody rozšíření:
siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();
Alternativně lze filtr zaregistrovat bez metody rozšíření:
siloHostBuilder.ConfigureServices(
services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());
Filtry volání podle agregace
Třída zrnitosti se může zaregistrovat jako filtr odstupňovaného volání a filtrovat všechna volání, která do ní byla provedena implementací IIncomingGrainCallFilter
, například takto:
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);
}
V předchozím příkladu se všechna volání GetFavoriteNumber
metody vrátí 38
místo 7
, protože návratová hodnota byla změněna filtrem.
Další případ použití pro filtry je v řízení přístupu, jak je znázorněno v tomto příkladu:
[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);
}
V předchozím příkladu lze metodu SpecialAdminOnlyOperation
volat pouze v případě, že "isAdmin"
je nastavena na true
.RequestContext
Tímto způsobem lze k autorizaci použít filtry odstupňované volání. V tomto příkladu je odpovědností volajícího, aby se zajistilo správné "isAdmin"
nastavení hodnoty a správné provedení ověřování. Všimněte si, že [AdminOnly]
atribut je zadán v metodě třídy grain. Je to proto, že ImplementationMethod
vlastnost vrací MethodInfo
implementaci, nikoli rozhraní. Filtr může také zkontrolovat InterfaceMethod
vlastnost.
Řazení filtru odstupňované volání
Filtry volání agregace se řídí definovaným pořadím:
IIncomingGrainCallFilter
implementace nakonfigurované v kontejneru injektáže závislostí v pořadí, v jakém jsou registrovány.- Filtr na úrovni zrnitosti, pokud se agregační interval implementuje
IIncomingGrainCallFilter
. - Implementace metody zrnitosti nebo implementace metody rozšíření zrnitosti
Každé volání IIncomingGrainCallContext.Invoke()
zapouzdřuje další definovaný filtr, aby každý filtr mohl spustit kód před a za dalším filtrem v řetězci a nakonec i samotnou metodu agregace.
Filtry odchozích hovorů
Filtry odchozích volání se podobají filtrům příchozích volání s velkým rozdílem v tom, že se volají na volajícím (klientovi) místo volaného (agregace).
Filtry odchozích volání implementují rozhraní, které má jednu metodu IOutgoingGrainCallFilter
:
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
Argument IOutgoingGrainCallContext předaný Invoke
metodě má následující tvar:
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; }
}
Metoda IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)
musí očekávat nebo vrátit výsledek IOutgoingGrainCallContext.Invoke()
spuštění dalšího nakonfigurovaného filtru a nakonec samotnou metodu agregace. Vlastnost Result
lze upravit po čekání na metodu Invoke()
. Volanou MethodInfo
metodu rozhraní lze získat přístup pomocí InterfaceMethod
vlastnosti. Filtry odchozích volání se volají pro všechna volání metod agregace a to zahrnuje volání systémových metod provedených metodou Orleans.
Konfigurace filtrů odchozích volání
IOutgoingGrainCallFilter
Implementace lze buď zaregistrovat na sila i klientech pomocí injektáže závislostí.
Delegáta je možné zaregistrovat jako filtr volání, například takto:
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;
}
});
Ve výše uvedeném kódu builder
může být instance ISiloHostBuilder nebo IClientBuilder.
Podobně lze třídu zaregistrovat jako filtr odchozích volání. Tady je příklad filtru volání agregace, který protokoluje výsledky každé metody agregace:
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;
}
}
}
Tento filtr pak můžete zaregistrovat pomocí AddOutgoingGrainCallFilter
metody rozšíření:
builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();
Alternativně lze filtr zaregistrovat bez metody rozšíření:
builder.ConfigureServices(
services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());
Stejně jako u příkladu builder
filtru volání delegáta může být instance ISiloHostBuilder IClientBuildernebo .
Případy použití
Převod výjimek
Pokud se v klientovi dostává deserializovaná výjimka, která byla vyvolána ze serveru, může se někdy místo skutečné výjimky zobrazit následující výjimka: TypeLoadException: Could not find Whatever.dll.
K tomu dochází v případě, že sestavení obsahující výjimku není pro klienta k dispozici. Řekněme například, že v implementacích agregačních jednotek používáte Entity Framework; EntityException
pak může být vyvolán. Klient na druhé straně neodkazuje (a neměl by) odkazovat EntityFramework.dll
, protože nezná podkladovou vrstvu přístupu k datům.
Když se klient pokusí deserializovat EntityException
, selže kvůli chybějící knihovně DLL; v důsledku toho TypeLoadException je vyvolán skrytí původní EntityException
.
Jeden může argumentovat, že to je docela v pořádku, protože klient by nikdy nezpracoval EntityException
; jinak by musel odkazovat EntityFramework.dll
.
Ale co když chce klient alespoň protokolovat výjimku? Problém je, že původní chybová zpráva je ztracena. Jedním ze způsobů, jak tento problém obejít, je zachytit výjimky na straně serveru a nahradit je prostými výjimkami typu Exception
, pokud je typ výjimky na straně klienta pravděpodobně neznámý.
Je však třeba mít na paměti jednu důležitou věc: chceme nahradit výjimku pouze v případě, že volající je klient zrnitosti. Nechceme nahradit výjimku, pokud je volající jiný agregační interval (nebo Orleans infrastruktura, která provádí také volání odstupňovaného intervalu, např. v agregačním intervalu GrainBasedReminderTable
).
Na straně serveru to lze provést pomocí průsečíku na úrovni sil:
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));
}
}
}
Tento filtr pak můžete zaregistrovat na silu:
siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();
Povolte filtr volání provedených klientem přidáním filtru odchozích volání:
clientBuilder.AddOutgoingGrainCallFilter(context =>
{
RequestContext.Set("IsExceptionConversionEnabled", true);
return context.Invoke();
});
Tímto způsobem klient informuje server, že chce použít převod výjimek.
Volání zrn z průsečíků
Z průsečíku je možné provádět odstupňovaná volání vložením IGrainFactory do třídy průsečíku:
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();
}