Filtry wywołań ziarna

Filtry wywołań ziarna zapewniają metodę przechwytywania wywołań ziarna. Filtry mogą wykonywać kod zarówno przed, jak i po wywołaniu ziarna. Jednocześnie można zainstalować wiele filtrów. Filtry są asynchroniczne i mogą modyfikować RequestContextargumenty i zwracaną wartość wywoływanej metody. Filtry mogą również sprawdzać MethodInfo wywoływaną metodę w klasie ziarna i mogą służyć do zgłaszania lub obsługi wyjątków.

Oto przykładowe użycie filtrów wywołań ziarna:

  • Autoryzacja: filtr może sprawdzić wywoływaną metodę oraz argumenty lub informacje o autoryzacji w elemecie , RequestContext aby określić, czy zezwolić wywołaniu na kontynuowanie.
  • Rejestrowanie/telemetria: filtr może rejestrować informacje i przechwytywać dane chronometrażu oraz inne statystyki dotyczące wywołania metody.
  • Obsługa błędów: filtr może przechwytywać wyjątki zgłaszane przez wywołanie metody i przekształcać je w inny wyjątek lub obsługiwać wyjątek podczas przechodzenia przez filtr.

Filtry są dostępne w dwóch wersjach:

  • Filtry połączeń przychodzących
  • Filtry połączeń wychodzących

Filtry połączeń przychodzących są wykonywane podczas odbierania połączenia. Filtry połączeń wychodzących są wykonywane podczas nawiązywania połączenia.

Filtry połączeń przychodzących

Filtry wywołań ziarna przychodzącego IIncomingGrainCallFilter implementują interfejs, który ma jedną metodę:

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

Argument IIncomingGrainCallContext przekazany do Invoke metody ma następujący kształt:

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) musi oczekiwać lub zwrócić wynik IIncomingGrainCallContext.Invoke() wykonania następnego skonfigurowanego filtru i ostatecznie samej metody ziarna. Właściwość Result można zmodyfikować po oczekiwaniu na metodę Invoke() . Właściwość ImplementationMethod zwraca MethodInfo klasę implementacji. Dostęp MethodInfo do metody interfejsu można uzyskać przy użyciu InterfaceMethod właściwości . Filtry wywołań ziarna są wywoływane dla wszystkich wywołań metod do ziarna i obejmuje to wywołania rozszerzeń ziarna (implementacje IGrainExtension), które są zainstalowane w ziarnie. Na przykład rozszerzenia ziarna są używane do implementowania Strumienie i tokenów anulowania. Dlatego należy się spodziewać, że wartość ImplementationMethod elementu nie zawsze jest metodą w samej klasie ziarna.

Konfigurowanie filtrów wywołań ziarna przychodzącego

Implementacje IIncomingGrainCallFilter programu można zarejestrować jako filtry dla całego silosu za pośrednictwem wstrzykiwania zależności lub można je zarejestrować jako filtry na poziomie ziarna za pośrednictwem implementacji ziarna IIncomingGrainCallFilter bezpośrednio.

Filtry wywołań ziarna całego silosu

Delegat można zarejestrować jako filtr wywołania ziarna całego silosu przy użyciu iniekcji zależności w następujący sposób:

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

Podobnie klasę można zarejestrować jako filtr wywołania ziarna przy użyciu metody pomocniczej AddIncomingGrainCallFilter . Oto przykład filtru wywołań ziarna, który rejestruje wyniki każdej metody ziarna:

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

Ten filtr można następnie zarejestrować przy użyciu AddIncomingGrainCallFilter metody rozszerzenia:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Alternatywnie można zarejestrować filtr bez metody rozszerzenia:

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

Filtry wywołań ziarna na ziarno

Klasa ziarna może zarejestrować się jako filtr wywołania ziarna i filtrować wszystkie wywołania wykonywane do niego, implementując IIncomingGrainCallFilter w następujący sposób:

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

W powyższym przykładzie wszystkie wywołania metody będą zwracane GetFavoriteNumber38 zamiast 7, ponieważ wartość zwracana została zmieniona przez filtr.

Inny przypadek użycia filtrów jest w kontroli dostępu, jak w tym przykładzie:

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

W powyższym przykładzie metodę SpecialAdminOnlyOperation można wywołać tylko wtedy, gdy "isAdmin" jest ustawiona na true wartość w obiekcie RequestContext. W ten sposób filtry wywołań ziarna mogą służyć do autoryzacji. W tym przykładzie obowiązkiem obiektu wywołującego jest upewnienie się, że "isAdmin" wartość jest ustawiona poprawnie i że uwierzytelnianie jest wykonywane poprawnie. Należy pamiętać, że [AdminOnly] atrybut jest określony w metodzie klasy ziarna. Dzieje się tak, ponieważ ImplementationMethod właściwość zwraca MethodInfo implementację, a nie interfejs. Filtr może również sprawdzić InterfaceMethod właściwość .

Porządkowanie filtru wywołań ziarna

Filtry wywołań ziarna są zgodne ze zdefiniowaną kolejnością:

  1. IIncomingGrainCallFilter implementacje skonfigurowane w kontenerze wstrzykiwania zależności, w kolejności, w której są zarejestrowane.
  2. Filtr na poziomie ziarna, jeśli ziarno implementuje IIncomingGrainCallFilter.
  3. Implementacja metody ziarna lub implementacja metody rozszerzenia ziarna.

Każde wywołanie metody IIncomingGrainCallContext.Invoke() hermetyzuje następny zdefiniowany filtr, tak aby każdy filtr miał szansę wykonać kod przed następnym filtrem w łańcuchu i po nim, a ostatecznie samą metodę ziarna.

Filtry połączeń wychodzących

Filtry wywołań ziarna wychodzącego są podobne do przychodzących filtrów wywołań ziarna, z główną różnicą jest to, że są wywoływane na wywołującym (kliencie), a nie wywoływane (ziarno).

Filtry wywołań ziarna wychodzącego implementują IOutgoingGrainCallFilter interfejs, który ma jedną metodę:

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

Argument IOutgoingGrainCallContext przekazany do Invoke metody ma następujący kształt:

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) musi oczekiwać lub zwrócić wynik IOutgoingGrainCallContext.Invoke() wykonania następnego skonfigurowanego filtru i ostatecznie samej metody ziarna. Właściwość Result można zmodyfikować po oczekiwaniu na metodę Invoke() . Dostęp MethodInfo do wywoływanej metody interfejsu można uzyskać przy użyciu InterfaceMethod właściwości . Filtry wywołań ziarna wychodzącego są wywoływane dla wszystkich wywołań metody do ziarna i obejmuje to wywołania metod systemowych wykonanych przez Orleansprogram .

Konfigurowanie filtrów połączeń ziarna wychodzącego

Implementacje IOutgoingGrainCallFilter programu można zarejestrować zarówno na silosach, jak i klientach przy użyciu wstrzykiwania zależności.

Delegata można zarejestrować jako filtr wywołań w następujący sposób:

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

W powyższym kodzie builder może być wystąpieniem ISiloHostBuilder elementu lub IClientBuilder.

Podobnie klasę można zarejestrować jako filtr wywołań ziarna wychodzącego. Oto przykład filtru wywołań ziarna, który rejestruje wyniki każdej metody ziarna:

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

Ten filtr można następnie zarejestrować przy użyciu AddOutgoingGrainCallFilter metody rozszerzenia:

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Alternatywnie można zarejestrować filtr bez metody rozszerzenia:

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

Podobnie jak w przypadku przykładu filtru wywołań delegowanych, builder może być wystąpieniem elementu ISiloHostBuilder lub IClientBuilder.

Przypadki użycia

Konwersja wyjątków

Jeśli wyjątek zgłoszony z serwera jest deserializowany na kliencie, czasami może wystąpić następujący wyjątek zamiast rzeczywistego: TypeLoadException: Could not find Whatever.dll.

Dzieje się tak, jeśli zestaw zawierający wyjątek nie jest dostępny dla klienta. Załóżmy na przykład, że używasz platformy Entity Framework w implementacjach ziarna; wówczas EntityException może zostać zgłoszony. Klient z drugiej strony nie odwołuje się (i nie powinien) odwoływać EntityFramework.dll się, ponieważ nie zna bazowej warstwy dostępu do danych.

Gdy klient próbuje wykonać deserializowanie EntityExceptionelementu , zakończy się niepowodzeniem z powodu brakującej biblioteki DLL; w konsekwencji TypeLoadException zgłaszany jest błąd ukrywania oryginalnego elementu EntityException.

Można twierdzić, że jest to całkiem w porządku, ponieważ klient nigdy nie będzie obsługiwał EntityException; w przeciwnym razie musiałby się odwoływać EntityFramework.dll.

Ale co zrobić, jeśli klient chce przynajmniej zarejestrować wyjątek? Problem polega na utracie oryginalnego komunikatu o błędzie. Jednym ze sposobów obejścia tego problemu jest przechwycenie wyjątków po stronie serwera i zastąpienie ich zwykłymi wyjątkami typu Exception , jeśli typ wyjątku jest prawdopodobnie nieznany po stronie klienta.

Należy jednak pamiętać o jednej ważnej kwestii: chcemy zastąpić wyjątek tylko wtedy, gdy obiekt wywołujący jest klientem ziarna. Nie chcemy zastępować wyjątku, jeśli obiekt wywołujący jest innym ziarnem (lub Orleans infrastrukturą, która wykonuje wywołania ziarna, np. na ziarno GrainBasedReminderTable ).

Po stronie serwera można to zrobić za pomocą przechwytywania na poziomie silosu:

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

Ten filtr można następnie zarejestrować w silosie:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Włącz filtr dla wywołań wykonanych przez klienta, dodając filtr połączeń wychodzących:

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

W ten sposób klient informuje serwer, że chce użyć konwersji wyjątków.

Wywoływanie ziarna z przechwytywania

Istnieje możliwość wykonania wywołań ziarna z przechwytnika przez wstrzyknięcie IGrainFactory do klasy przechwytywania:

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