Aanroepfilters voor graan
Grain-aanroepfilters bieden een middel voor het onderscheppen van graanoproepen. Filters kunnen code uitvoeren voor en na een graanaanroep. Meerdere filters kunnen tegelijkertijd worden geïnstalleerd. Filters zijn asynchroon en kunnen wijzigen RequestContext, argumenten en de retourwaarde van de methode die wordt aangeroepen. Filters kunnen ook de MethodInfo methode die wordt aangeroepen op de graanklasse inspecteren en kunnen worden gebruikt voor het genereren of verwerken van uitzonderingen.
Enkele voorbeelden van het gebruik van graanoproepfilters zijn:
- Autorisatie: een filter kan de methode die wordt aangeroepen en de argumenten of bepaalde autorisatiegegevens in de
RequestContext
filter controleren om te bepalen of de aanroep al dan niet mag worden voortgezet. - Logboekregistratie/telemetrie: een filter kan informatie vastleggen en timinggegevens en andere statistieken over methode-aanroep vastleggen.
- Foutafhandeling: een filter kan uitzonderingen onderscheppen die worden gegenereerd door een methode-aanroep en deze transformeren in een andere uitzondering of de uitzondering verwerken terwijl het filter wordt doorgegeven.
Filters zijn in twee varianten beschikbaar:
- Binnenkomende oproepfilters
- Filters voor uitgaande oproepen
Binnenkomende oproepfilters worden uitgevoerd bij het ontvangen van een oproep. Uitgaande oproepfilters worden uitgevoerd bij het maken van een oproep.
Binnenkomende oproepfilters
Binnenkomende aanroepfilters implementeren de IIncomingGrainCallFilter interface, die één methode heeft:
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
Het IIncomingGrainCallContext argument dat aan de Invoke
methode is doorgegeven, heeft de volgende vorm:
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; }
}
De IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)
methode moet wachten of het resultaat retourneren van het uitvoeren van IIncomingGrainCallContext.Invoke()
het volgende geconfigureerde filter en uiteindelijk de graanmethode zelf. De Result
eigenschap kan worden gewijzigd nadat u de Invoke()
methode hebt gewacht. De ImplementationMethod
eigenschap retourneert de MethodInfo
implementatieklasse. De MethodInfo
interfacemethode kan worden geopend met behulp van de InterfaceMethod
eigenschap. Grain-aanroepfilters worden aangeroepen voor alle methode-aanroepen naar een graan en dit omvat aanroepen naar graanextensies (implementaties van IGrainExtension
) die in het graan zijn geïnstalleerd. Graanextensies worden bijvoorbeeld gebruikt voor het implementeren van streams en annuleringstokens. Daarom moet worden verwacht dat de waarde van ImplementationMethod
niet altijd een methode is in de graanklasse zelf.
Binnenkomende aanroepfilters configureren
Implementaties van IIncomingGrainCallFilter kunnen worden geregistreerd als silobrede filters via afhankelijkheidsinjectie of ze kunnen worden geregistreerd als filters op graanniveau via een korrel die rechtstreeks worden geïmplementeerd IIncomingGrainCallFilter
.
Silobrede aanroepfilters
Een gemachtigde kan als een silobreed aanroepfilter worden geregistreerd met behulp van afhankelijkheidsinjectie als volgt:
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;
}
});
Op dezelfde manier kan een klasse worden geregistreerd als een graanoproepfilter met behulp van de AddIncomingGrainCallFilter helpermethode. Hier volgt een voorbeeld van een graanaanroepfilter waarmee de resultaten van elke graanmethode worden vastgeslagen:
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;
}
}
}
Dit filter kan vervolgens worden geregistreerd met behulp van de AddIncomingGrainCallFilter
extensiemethode:
siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();
U kunt het filter ook registreren zonder de extensiemethode:
siloHostBuilder.ConfigureServices(
services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());
Aanroepfilters per graan
Een graanklasse kan zichzelf registreren als een korreloproepfilter en alle aanroepen filteren die ernaar worden gedaan door dit als volgt te implementeren 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);
}
In het bovenstaande voorbeeld worden alle aanroepen naar de GetFavoriteNumber
methode geretourneerd 38
in plaats van 7
, omdat de retourwaarde is gewijzigd door het filter.
Een ander gebruiksvoorbeeld voor filters is in toegangsbeheer, zoals in dit voorbeeld:
[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);
}
In het bovenstaande voorbeeld kan de SpecialAdminOnlyOperation
methode alleen worden aangeroepen als "isAdmin"
deze is ingesteld true
op in de RequestContext
. Op deze manier kunnen graanoproepfilters worden gebruikt voor autorisatie. In dit voorbeeld is het de verantwoordelijkheid van de aanroeper om ervoor te zorgen dat de "isAdmin"
waarde correct is ingesteld en dat de verificatie correct wordt uitgevoerd. Houd er rekening mee dat het [AdminOnly]
kenmerk is opgegeven voor de grain-klassemethode. Dit komt doordat de ImplementationMethod
eigenschap de MethodInfo
implementatie retourneert, niet de interface. Het filter kan ook de InterfaceMethod
eigenschap controleren.
Volgorde van aanroepfilter voor graanoproep
Filters voor aanroepen van graan volgen een gedefinieerde volgorde:
IIncomingGrainCallFilter
implementaties die zijn geconfigureerd in de container voor afhankelijkheidsinjectie, in de volgorde waarin ze zijn geregistreerd.- Filter op graanniveau, als het graan wordt geïmplementeerd
IIncomingGrainCallFilter
. - Implementatie van grainmethode of implementatie van de graanextensiemethode.
Elke aanroep om IIncomingGrainCallContext.Invoke()
het volgende gedefinieerde filter in te kapselen, zodat elk filter de kans heeft om code voor en na het volgende filter in de keten uit te voeren en uiteindelijk de graanmethode zelf.
Filters voor uitgaande oproepen
Uitgaande graanaanroepfilters zijn vergelijkbaar met binnenkomende aanroepfilters, waarbij het belangrijkste verschil is dat ze worden aangeroepen in de aanroeper (client) in plaats van de aanroeper (grain).
Uitgaande graanoproepfilters implementeren de IOutgoingGrainCallFilter
interface, die één methode heeft:
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
Het IOutgoingGrainCallContext argument dat aan de Invoke
methode is doorgegeven, heeft de volgende vorm:
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; }
}
De IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)
methode moet wachten of het resultaat retourneren van het uitvoeren van IOutgoingGrainCallContext.Invoke()
het volgende geconfigureerde filter en uiteindelijk de graanmethode zelf. De Result
eigenschap kan worden gewijzigd nadat u de Invoke()
methode hebt gewacht. De MethodInfo
interfacemethode die wordt aangeroepen, kan worden geopend met behulp van de InterfaceMethod
eigenschap. Uitgaande aanroepfilters worden aangeroepen voor alle methode-aanroepen naar een graan en dit omvat aanroepen naar systeemmethoden die worden gemaakt door Orleans.
Uitgaande aanroepfilters configureren
Implementaties van IOutgoingGrainCallFilter
kunnen worden geregistreerd op zowel silo's als clients met behulp van afhankelijkheidsinjectie.
Een gemachtigde kan als een gespreksfilter als volgt worden geregistreerd:
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;
}
});
In de bovenstaande code builder
kan dit een exemplaar zijn van ISiloHostBuilder of IClientBuilder.
Op dezelfde manier kan een klasse worden geregistreerd als een uitgaande aanroepfilter. Hier volgt een voorbeeld van een graanaanroepfilter waarmee de resultaten van elke graanmethode worden vastgeslagen:
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;
}
}
}
Dit filter kan vervolgens worden geregistreerd met behulp van de AddOutgoingGrainCallFilter
extensiemethode:
builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();
U kunt het filter ook registreren zonder de extensiemethode:
builder.ConfigureServices(
services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());
Net als bij het voorbeeld van het gespreksfilter voor gemachtigden, builder
kan dit een exemplaar zijn van of ISiloHostBuilder IClientBuilder.
Gebruiksgevallen
Uitzonderingsconversie
Wanneer een uitzondering die van de server is gegenereerd, wordt gedeserialiseerd op de client, krijgt u soms de volgende uitzondering in plaats van de werkelijke uitzondering: TypeLoadException: Could not find Whatever.dll.
Dit gebeurt als de assembly met de uitzondering niet beschikbaar is voor de client. Stel dat u Entity Framework gebruikt in uw grain-implementaties; dan kan er een EntityException
gegooid worden. De client daarentegen verwijst niet (en mag niet) verwijzen EntityFramework.dll
omdat deze de onderliggende gegevenstoegangslaag niet kent.
Wanneer de client probeert deserialiseren van de EntityException
client, mislukt deze vanwege het ontbrekende DLL-bestand. Als gevolg hiervan wordt er een TypeLoadException gegooid die het oorspronkelijke EntityException
bestand verbergt.
Men kan beweren dat dit vrij goed is, omdat de client de EntityException
; anders zou het moeten verwijzen EntityFramework.dll
.
Maar wat als de client ten minste de uitzondering wil registreren? Het probleem is dat het oorspronkelijke foutbericht verloren gaat. U kunt dit probleem omzeilen door uitzonderingen aan de serverzijde te onderscheppen en te vervangen door gewone uitzonderingen van het type Exception
als het uitzonderingstype waarschijnlijk onbekend is aan de clientzijde.
Er is echter een belangrijk ding dat we in gedachten moeten houden: we willen alleen een uitzondering vervangen als de beller de graanclient is. We willen geen uitzondering vervangen als de aanroeper een ander graan is (of de Orleans infrastructuur die ook graanaanroepen doet; bijvoorbeeld op het GrainBasedReminderTable
graan).
Aan de serverzijde kan dit worden gedaan met een interceptor op siloniveau:
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));
}
}
}
Dit filter kan vervolgens op de silo worden geregistreerd:
siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();
Schakel het filter in voor aanroepen die door de client worden gedaan door een filter voor uitgaande oproepen toe te voegen:
clientBuilder.AddOutgoingGrainCallFilter(context =>
{
RequestContext.Set("IsExceptionConversionEnabled", true);
return context.Invoke();
});
Op deze manier vertelt de client de server dat deze uitzonderingsconversie wil gebruiken.
Korrels van snijpunten aanroepen
Het is mogelijk om graanaanroepen van een interceptor te maken door in de interceptorklasse te injecteren 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();
}