Udostępnij przez


Filtry wywołań ziarna

Filtry wywołań ziarna umożliwiają przechwytywanie wywołań ziarna. Filtry mogą wykonywać kod zarówno przed, jak i po wywołaniu jednostki przetwarzającej. Można zainstalować wiele filtrów jednocześnie. Filtry są asynchroniczne i mogą modyfikować RequestContext, argumenty oraz 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 zastosowania 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 oraz przechwytywać dane dotyczące czasu i inne statystyki związane z wywołaniem metody.
  • Obsługa błędów: filtr może przechwytywać wyjątki rzucane podczas wywołania metody i przekształcać je w inne wyjątki lub obsługiwać je, gdy przechodzą przez filtr.

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

  • 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 przychodzących wywołań ziarna implementują interfejs IIncomingGrainCallFilter, który zawiera 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 await lub zwrócić wynik IIncomingGrainCallContext.Invoke() dla wykonania następnego skonfigurowanego filtru i ostatecznie metody ziarna. Możesz zmodyfikować właściwość Result po ukończeniu oczekiwania na metodę Invoke(). Właściwość ImplementationMethod zwraca MethodInfo klasy implementacji. Dostęp do MethodInfo metody interfejsu można uzyskać przy użyciu właściwości InterfaceMethod. Filtry wywołań są wywoływane dla wszystkich wywołań metody do ziarna, w tym wywołań rozszerzeń dla ziarna (implementacje IGrainExtension) zainstalowanych w nim. Na przykład Orleans wykorzystuje rozszerzenia modelu ziarna do implementacji strumieni i tokenów przerwania. W związku z tym należy oczekiwać, że wartość ImplementationMethod nie zawsze jest metodą w samej klasie ziarna.

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

Implementacje IIncomingGrainCallFilter można zarejestrować jako filtry dla całego silosu za pomocą wstrzykiwania zależności lub jako filtry na poziomie ziarna poprzez bezpośrednią implementację IIncomingGrainCallFilter przez ziarno.

Filtry wywołań ziarna całego silosu

Delegata można zarejestrować jako filtr wywołań ziarna na poziomie całego silosu, używając wstrzykiwania 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, za pomocą metody pomocniczej AddIncomingGrainCallFilter, można zarejestrować klasę jako filtr ziarna podczas wywołań. 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 poprzednim przykładzie wszystkie wywołania GetFavoriteNumber metody zwracają 38 zamiast 7 , ponieważ filtr zmienił wartość zwracaną.

Innym przypadkiem użycia filtrów jest kontrola dostępu, jak pokazano 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 poprzednim przykładzie metodę SpecialAdminOnlyOperation można wywołać tylko wtedy, gdy "isAdmin" jest ustawiona na wartość true w RequestContext. W ten sposób można użyć filtrów wywołań ziarna do celów autoryzacyjnych. W tym przykładzie odpowiedzialność wywołującego polega na zapewnieniu, że wartość "isAdmin" 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 IIncomingGrainCallContext.Invoke() hermetyzuje następny zdefiniowany filtr, umożliwiając każdemu filtrowi wykonanie kodu przed i po następnym filtrze w łańcuchu oraz ostatecznie samej metodzie grain.

Filtry połączeń wychodzących

Filtry wywołań ziarna wychodzących są podobne do filtrów wywołań ziarna przychodzących. Główna różnica polega na tym, że są one wywoływane na obiekcie wywołującym (kliencie), a nie na obiekcie wywoływanym (ziarnie).

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 await lub zwrócić wynik IOutgoingGrainCallContext.Invoke() dla wykonania następnego skonfigurowanego filtru i ostatecznie metody ziarna. Możesz zmodyfikować właściwość Result po ukończeniu oczekiwania na metodę Invoke(). Możesz uzyskać dostęp do MethodInfo metody interfejsu wywoływanej InterfaceMethod przy użyciu właściwości . Filtry wychodzących wywołań do ziarna są uruchamiane dla wszystkich wywołań metod do ziarna, w tym wywołań metod systemowych wykonywanych przez Orleans.

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

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

Zarejestruj delegata jako filtr do 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ć albo instancją ISiloHostBuilder lub IClientBuilder.

Podobnie można zarejestrować klasę jako filtr wywołania 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

Gdy wyjątek zgłoszony przez serwer jest deserializowany na kliencie, 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 zboża; EntityException może zostać zgłoszony. Z drugiej strony klient nie odwołuje się do EntityFramework.dll (i nie powinien), ponieważ nie zna bazowej warstwy dostępu do danych.

Gdy klient próbuje zdeserializować EntityException, kończy się niepowodzeniem z powodu brakującej biblioteki DLL. W konsekwencji TypeLoadException zostaje rzucony, ukrywając oryginalny EntityException.

Można argumentować, że jest to dopuszczalne, ponieważ klient nigdy nie obsłuży EntityException; w przeciwnym razie musiałby się odnosić do 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: chcesz zastąpić wyjątek tylko wtedy, gdy obiekt wywołujący jest klientem 'grain'. Nie chcesz zastępować wyjątku, jeśli obiekt wywołujący jest innym ziarnem (lub Orleans infrastrukturą wykonującą wywołania między ziarnami, na przykład na ziarnie GrainBasedReminderTable).

Po stronie serwera można to zrobić za pomocą interceptora 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 skorzystać z konwersji wyjątków.

Przywoływanie modułów z przechwytujących komponentów

Wywołania ziarna z interceptora można wykonać przez wstrzyknięcie IGrainFactory do klasy interceptora.

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