Filter för kornanrop
Filter för kornanrop är ett sätt att fånga upp kornanrop. Filter kan köra kod både före och efter ett kornigt anrop. Flera filter kan installeras samtidigt. Filter är asynkrona och kan ändra RequestContext, argument och returvärdet för metoden som anropas. Filter kan också kontrollera vilken MethodInfo metod som anropas i kornklassen och kan användas för att generera eller hantera undantag.
Några exempel på användningar av filter för kornanrop är:
- Auktorisering: ett filter kan kontrollera vilken metod som anropas och argumenten eller viss auktoriseringsinformation i
RequestContext
för att avgöra om anropet ska kunna fortsätta eller inte. - Loggning/telemetri: ett filter kan logga information och samla in tidsdata och annan statistik om metodanrop.
- Felhantering: ett filter kan fånga upp undantag som genereras av ett metodanrop och omvandla det till ett annat undantag eller hantera undantaget när det passerar genom filtret.
Filter finns i två varianter:
- Filter för inkommande samtal
- Filter för utgående samtal
Inkommande samtalsfilter körs när du tar emot ett samtal. Utgående samtalsfilter körs när ett anrop görs.
Filter för inkommande samtal
Filter för inkommande kornanrop implementerar IIncomingGrainCallFilter gränssnittet, som har en metod:
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
Argumentet IIncomingGrainCallContext som skickas Invoke
till metoden har följande form:
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; }
}
Metoden IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)
måste invänta eller returnera resultatet av IIncomingGrainCallContext.Invoke()
för att köra nästa konfigurerade filter och slutligen själva kornmetoden. Egenskapen Result
kan ändras efter att du har väntat på Invoke()
metoden. Egenskapen ImplementationMethod
returnerar MethodInfo
implementeringsklassen. Du MethodInfo
kan komma åt gränssnittsmetoden med hjälp av egenskapen InterfaceMethod
. Filter för kornanrop anropas för alla metodanrop till ett korn och detta inkluderar anrop till korntillägg (implementeringar av IGrainExtension
) som är installerade i kornigheten. Till exempel används korntillägg för att implementera Flöden- och annulleringstoken. Därför bör det förväntas att värdet för ImplementationMethod
inte alltid är en metod i själva kornklassen.
Konfigurera filter för inkommande kornsamtal
Implementeringar av IIncomingGrainCallFilter kan antingen registreras som siloomfattande filter via beroendeinmatning eller så kan de registreras som filter på kornnivå via ett korn som implementerar IIncomingGrainCallFilter
direkt.
Filter för Silo-wide grain-anrop
Ett ombud kan registreras som ett siloomfattande anropsfilter med beroendeinmatning så här:
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;
}
});
På samma sätt kan en klass registreras som ett kornigt anropsfilter med hjälpmetoden AddIncomingGrainCallFilter . Här är ett exempel på ett kornigt anropsfilter som loggar resultatet av varje kornmetod:
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;
}
}
}
Det här filtret kan sedan registreras med hjälp av AddIncomingGrainCallFilter
tilläggsmetoden:
siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();
Alternativt kan filtret registreras utan tilläggsmetoden:
siloHostBuilder.ConfigureServices(
services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());
Anropsfilter per kornnivå
En kornklass kan registrera sig som ett kornanropsfilter och filtrera alla anrop som görs till den genom att implementera IIncomingGrainCallFilter
så här:
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);
}
I exemplet ovan returneras 38
alla anrop till GetFavoriteNumber
metoden i stället för 7
, eftersom returvärdet har ändrats av filtret.
Ett annat användningsfall för filter finns i åtkomstkontroll, som i det här exemplet:
[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);
}
I exemplet ovan SpecialAdminOnlyOperation
kan metoden bara anropas om "isAdmin"
den är inställd true
på RequestContext
i . På så sätt kan filter för kornanrop användas för auktorisering. I det här exemplet är det anroparens ansvar att se till att "isAdmin"
värdet har angetts korrekt och att autentiseringen utförs korrekt. Observera att [AdminOnly]
attributet anges på metoden för kornklass. Det beror på att ImplementationMethod
egenskapen returnerar MethodInfo
implementeringen, inte gränssnittet. Filtret kan också kontrollera egenskapen InterfaceMethod
.
Kornig anropsfilterordning
Filter för kornanrop följer en definierad ordning:
IIncomingGrainCallFilter
implementeringar som konfigurerats i containern för beroendeinmatning i den ordning de registreras.- Kornnivåfilter, om kornet implementerar
IIncomingGrainCallFilter
. - Implementering av kornmetod eller metodimplementering av korntillägg.
Varje anrop för att IIncomingGrainCallContext.Invoke()
kapsla in nästa definierade filter så att varje filter har en chans att köra kod före och efter nästa filter i kedjan och slutligen själva kornmetoden.
Filter för utgående samtal
Filter för utgående kornanrop liknar inkommande kornanropsfilter, där den största skillnaden är att de anropas på anroparen (klienten) i stället för anroparen (korn).
Filter för utgående kornanrop implementerar IOutgoingGrainCallFilter
gränssnittet, som har en metod:
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
Argumentet IOutgoingGrainCallContext som skickas Invoke
till metoden har följande form:
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; }
}
Metoden IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)
måste invänta eller returnera resultatet av IOutgoingGrainCallContext.Invoke()
för att köra nästa konfigurerade filter och slutligen själva kornmetoden. Egenskapen Result
kan ändras efter att du har väntat på Invoke()
metoden. Den MethodInfo
gränssnittsmetod som anropas kan nås med hjälp av InterfaceMethod
egenskapen . Filter för utgående kornanrop anropas för alla metodanrop till ett korn och detta inkluderar anrop till systemmetoder som görs av Orleans.
Konfigurera filter för utgående kornsamtal
Implementeringar av kan antingen registreras på både silor och klienter med hjälp av IOutgoingGrainCallFilter
beroendeinmatning.
Ett ombud kan registreras som ett anropsfilter så här:
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;
}
});
I koden ovan builder
kan vara antingen en instans av ISiloHostBuilder eller IClientBuilder.
På samma sätt kan en klass registreras som ett utgående kornanropsfilter. Här är ett exempel på ett kornigt anropsfilter som loggar resultatet av varje kornmetod:
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;
}
}
}
Det här filtret kan sedan registreras med hjälp av AddOutgoingGrainCallFilter
tilläggsmetoden:
builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();
Alternativt kan filtret registreras utan tilläggsmetoden:
builder.ConfigureServices(
services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());
Precis som med exemplet builder
med anropsfilter för ombud kan vara en instans av antingen ISiloHostBuilder eller IClientBuilder.
Användningsfall
Undantagskonvertering
När ett undantag som har genererats från servern deserialiseras på klienten kan du ibland få följande undantag i stället för det faktiska: TypeLoadException: Could not find Whatever.dll.
Detta inträffar om sammansättningen som innehåller undantaget inte är tillgänglig för klienten. Anta till exempel att du använder Entity Framework i dina kornimplementeringar. sedan kan en EntityException
kastas. Klienten å andra sidan refererar inte till (och bör inte) EntityFramework.dll
eftersom den inte känner till det underliggande dataåtkomstskiktet.
När klienten försöker deserialisera EntityException
kommer den att misslyckas på grund av den saknade DLL:en, vilket innebär att en TypeLoadException genereras och döljer den ursprungliga EntityException
.
Man kan hävda att detta är ganska okej eftersom klienten aldrig skulle hantera EntityException
; annars skulle det behöva referera EntityFramework.dll
till .
Men vad händer om klienten åtminstone vill logga undantaget? Problemet är att det ursprungliga felmeddelandet går förlorat. Ett sätt att kringgå det här problemet är att fånga upp undantag på serversidan och ersätta dem med vanliga undantag av typen Exception
om undantagstypen förmodligen är okänd på klientsidan.
Det finns dock en viktig sak som vi måste tänka på: vi vill bara ersätta ett undantag om anroparen är kornklienten. Vi vill inte ersätta ett undantag om anroparen är ett annat korn (eller infrastrukturen Orleans som gör kornanrop också, t.ex. på kornet GrainBasedReminderTable
).
På serversidan kan detta göras med en silonivåavlyssnare:
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));
}
}
}
Det här filtret kan sedan registreras på silon:
siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();
Aktivera filtret för anrop som görs av klienten genom att lägga till ett utgående anropsfilter:
clientBuilder.AddOutgoingGrainCallFilter(context =>
{
RequestContext.Set("IsExceptionConversionEnabled", true);
return context.Invoke();
});
På så sätt meddelar klienten servern att den vill använda undantagskonvertering.
Anropa korn från interceptorer
Det är möjligt att göra kornanrop från en interceptor genom att IGrainFactory mata in i interceptor-klassen:
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();
}