Filter für Grainaufrufe

Filter für Grainaufrufe bieten ein Mittel zum Abfangen von Grainaufrufen. Filter können Code sowohl vor als auch nach einem Grainaufruf ausführen. Mehrere Filter können gleichzeitig installiert werden. Filter sind asynchron und können RequestContext, Argumente und den Rückgabewert der aufgerufenen Methode ändern. Filter können auch den MethodInfo der Methode überprüfen, die für die Grainklasse aufgerufen wird, und können verwendet werden, um Ausnahmen auszulösen oder zu behandeln.

Einige Beispiele für die Verwendung von Filtern für Grainaufrufe sind:

  • Autorisierung: Ein Filter kann die aufgerufene Methode und die Argumente oder einige Autorisierungsinformationen im RequestContext überprüfen, um zu bestimmen, ob der Aufruf fortgesetzt werden soll.
  • Protokollierung/Telemetrie: Ein Filter kann Informationen protokollieren und Zeitdaten und andere Statistiken zum Methodenaufruf erfassen.
  • Fehlerbehandlung: Ein Filter kann Ausnahmen abfangen, die von einem Methodenaufruf ausgelöst werden, und sie in eine andere Ausnahme transformieren oder die Ausnahme behandeln, während sie den Filter durchläuft.

Es gibt zwei Arten von Filtern:

  • Filter für eingehende Aufrufe
  • Filter für ausgehende Aufrufe

Filter für eingehende Aufrufe werden beim Empfangen eines Aufrufs ausgeführt. Beim Tätigen eines Aufrufs werden Filter für ausgehende Aufrufe ausgeführt.

Filter für eingehende Aufrufe

Filter für eingehende Grainaufrufe implementieren die IIncomingGrainCallFilter-Schnittstelle, die über eine Methode verfügt:

public interface IIncomingGrainCallFilter
{
    Task Invoke(IIncomingGrainCallContext context);
}

Das an die Invoke-Methode übergebene IIncomingGrainCallContext-Argument hat die folgende 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; }
}

Die IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)-Methode muss das Ergebnis von IIncomingGrainCallContext.Invoke() abwarten oder zurückgeben, um den nächsten konfigurierten Filter und schließlich die Grainmethode selbst auszuführen. Die Result-Eigenschaft kann nach dem Abwarten der Invoke()-Methode geändert werden. Die ImplementationMethod-Eigenschaft gibt die MethodInfo der Implementierungsklasse zurück. Auf die MethodInfo der Schnittstellenmethode kann mithilfe der InterfaceMethod-Eigenschaft zugegriffen werden. Filter für Grainaufrufe werden für alle Methodenaufrufe eines Grains aufgerufen, und dies schließt Aufrufe von Grainerweiterungen (Implementierungen von IGrainExtension) ein, die im Grain installiert sind. Beispielsweise werden Grainerweiterungen verwendet, um Streams und Abbruchtoken zu implementieren. Daher sollte erwartet werden, dass der Wert von ImplementationMethod nicht immer eine Methode in der Grainklasse selbst ist.

Konfigurieren von Filtern für eingehende Grainaufrufe

Implementierungen von IIncomingGrainCallFilter können entweder als siloweite Filter über Dependency Injection oder direkt als Filter auf Grainebene über ein IIncomingGrainCallFilter implementierendes Grain registriert werden.

Siloweite Filter für Grainaufrufe

Ein Delegat kann mit Dependency Injection wie folgt als siloweiter Filter für Grainaufrufe registriert werden:

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;
    }
});

Ebenso kann eine Klasse mit der AddIncomingGrainCallFilter-Hilfsmethode als Filter für Grainaufrufe registriert werden. Hier sehen Sie ein Beispiel für einen Filter für Grainaufrufe, der die Ergebnisse jeder Grainmethode protokolliert:

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;
        }
    }
}

Dieser Filter kann dann mit der AddIncomingGrainCallFilter-Erweiterungsmethode registriert werden:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Alternativ kann der Filter auch ohne die Erweiterungsmethode registriert werden:

siloHostBuilder.ConfigureServices(
    services => services.AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>());

Filter für Grainaufrufe pro Grain

Eine Grainklasse kann sich selbst als Filter für Grainaufrufe registrieren und alle an sie gerichteten Aufrufe filtern, indem sie wie folgt IIncomingGrainCallFilter implementiert:

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);
}

Im obigen Beispiel geben alle Aufrufe der GetFavoriteNumber-Methode anstelle von 387 zurück, da der Rückgabewert vom Filter geändert wurde.

Ein weiterer Anwendungsfall für Filter ist die Zugriffssteuerung, wie in diesem Beispiel:

[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);
}

Im obigen Beispiel kann die SpecialAdminOnlyOperation-Methode nur aufgerufen werden, wenn "isAdmin" im RequestContext auf true festgelegt ist. Auf diese Weise können Filter für Grainaufrufe für die Autorisierung verwendet werden. In diesem Beispiel ist der Aufrufer dafür verantwortlich, sicherzustellen, dass der "isAdmin"-Wert richtig festgelegt und die Authentifizierung ordnungsgemäß ausgeführt wird. Beachten Sie, dass das [AdminOnly]-Attribut für die Grainklassenmethode angegeben wird. Dies liegt daran, dass die ImplementationMethod-Eigenschaft die MethodInfo der Implementierung zurückgibt, nicht die Schnittstelle. Der Filter könnte auch die InterfaceMethod-Eigenschaft überprüfen.

Reihenfolge des Filters für Grainaufrufe

Filter für Grainaufrufe folgen einer definierten Reihenfolge:

  1. IIncomingGrainCallFilter-Implementierungen, die im Dependency Injection-Container in der Reihenfolge konfiguriert werden, in der sie registriert werden.
  2. Filter auf Grainebene, wenn das Grain IIncomingGrainCallFilter implementiert.
  3. Implementierung der Grainmethode oder Implementierung der Grainerweiterungsmethode.

Jeder Aufruf von IIncomingGrainCallContext.Invoke() kapselt den nächsten definierten Filter, sodass jeder Filter die Möglichkeit hat, Code vor und nach dem nächsten Filter in der Kette und schließlich die Grainmethode selbst auszuführen.

Filter für ausgehende Aufrufe

Filter für ausgehende Grainaufrufe ähneln Filtern für eingehende Grainaufrufe, wobei der Hauptunterschied darin besteht, dass sie auf dem Aufrufer (Client) und nicht auf dem Aufgerufenen (Grain) aufgerufen werden.

Filter für ausgehende Grainaufrufe implementieren die IOutgoingGrainCallFilter-Schnittstelle, die über eine Methode verfügt:

public interface IOutgoingGrainCallFilter
{
    Task Invoke(IOutgoingGrainCallContext context);
}

Das an die Invoke-Methode übergebene IOutgoingGrainCallContext-Argument hat die folgende 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; }
}

Die IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)-Methode muss das Ergebnis von IOutgoingGrainCallContext.Invoke() abwarten oder zurückgeben, um den nächsten konfigurierten Filter und schließlich die Grainmethode selbst auszuführen. Die Result-Eigenschaft kann nach dem Abwarten der Invoke()-Methode geändert werden. Auf die MethodInfo der aufgerufenen Schnittstellenmethode kann mithilfe der InterfaceMethod-Eigenschaft zugegriffen werden. Filter für ausgehende Grainaufrufe werden für alle Methodenaufrufe eines Grains aufgerufen, und dies schließt Aufrufe von Systemmethoden ein, die von Orleans ausgeführt werden.

Konfigurieren von Filtern für ausgehende Grainaufrufe

Implementierungen von IOutgoingGrainCallFilter können sowohl auf Silos als auch Clients mit Dependency Injection registriert werden.

Ein Delegat kann wie folgt als Aufruffilter registriert werden:

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;
    }
});

Im obigen Code kann builder entweder eine Instanz von ISiloHostBuilder oder IClientBuilder sein.

Ebenso kann eine Klasse als Filter für ausgehende Grainaufrufe registriert werden. Hier sehen Sie ein Beispiel für einen Filter für Grainaufrufe, der die Ergebnisse jeder Grainmethode protokolliert:

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;
        }
    }
}

Dieser Filter kann dann mit der AddOutgoingGrainCallFilter-Erweiterungsmethode registriert werden:

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Alternativ kann der Filter auch ohne die Erweiterungsmethode registriert werden:

builder.ConfigureServices(
    services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());

Wie beim Beispiel für den Delegataufruffilter kann builder eine Instanz von ISiloHostBuilder oder IClientBuilder sein.

Anwendungsfälle

Ausnahmekonvertierung

Wenn eine vom Server ausgelöste Ausnahme auf dem Client deserialisiert wird, erhalten Sie manchmal die folgende Ausnahme anstelle der tatsächlichen Ausnahme: TypeLoadException: Could not find Whatever.dll.

Dies geschieht, wenn die Assembly, die die Ausnahme enthält, für den Client nicht verfügbar ist. Angenommen, Sie verwenden Entity Framework in Ihren Grainimplementierungen, dann kann eine EntityException ausgelöst werden. Der Client hingegen verweist nicht auf EntityFramework.dll (und sollte es auch nicht), da er die zugrunde liegende Datenzugriffsebene nicht kennt.

Wenn der Client versucht, die EntityException zu deserialisieren, tritt aufgrund der fehlenden DLL ein Fehler auf. Als Konsequenz wird eine TypeLoadException ausgelöst, die die ursprüngliche EntityException verbirgt.

Es lässt sich argumentieren, dass dies ziemlich in Ordnung ist, da der Client niemals die EntityException behandeln würde; andernfalls müsste er auf EntityFramework.dll verweisen.

Was aber, wenn der Client die Ausnahme zumindest protokollieren möchte? Das Problem ist, dass die ursprüngliche Fehlermeldung verloren geht. Eine Möglichkeit, dieses Problem zu umgehen, besteht darin, serverseitige Ausnahmen abzufangen und durch einfache Ausnahmen vom Typ Exception zu ersetzen, wenn der Ausnahmetyp auf der Clientseite vermutlich unbekannt ist.

Wir müssen jedoch einen wichtigen Aspekt beachten: Wir möchten eine Ausnahme nur ersetzen, wenn der Aufrufer der Grainclient ist. Wir möchten keine Ausnahme ersetzen, wenn der Aufrufer ein anderes Grain ist (oder die Orleans-Infrastruktur, die auch Grainaufrufe durchführt, z. B. auf dem GrainBasedReminderTable-Grain).

Serverseitig kann dies mit einem Interceptor auf Siloebene erfolgen:

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));
        }
    }
}

Dieser Filter kann dann im Silo registriert werden:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Aktivieren Sie den Filter für vom Client getätigte Aufrufe, indem Sie einen Filter für ausgehende Aufrufe hinzufügen:

clientBuilder.AddOutgoingGrainCallFilter(context =>
{
    RequestContext.Set("IsExceptionConversionEnabled", true);
    return context.Invoke();
});

Auf diese Weise teilt der Client dem Server mit, dass er die Ausnahmekonvertierung verwenden möchte.

Aufrufen von Grains durch Interceptors

Es ist möglich, Grainaufrufe von einem Interceptor auszuführen, indem IGrainFactory in die Interceptorklasse eingefügt wird:

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();
}