Фильтры вызовов зерна

Фильтры вызовов зерна предоставляют средства для перехвата вызовов зерна. Фильтры могут выполнять код как до, так и после вызова зерна. Одновременно можно установить несколько фильтров. Фильтры являются асинхронными и могут изменять RequestContext, аргументы и возвращаемое значение вызываемого метода. Фильтры также могут проверять MethodInfo метод, вызываемый в классе зерна, и можно использовать для создания или обработки исключений.

Ниже приведены примеры использования фильтров вызовов зерна:

  • Авторизация: фильтр может проверить вызываемый метод, а также аргументы или некоторые сведения о авторизации, RequestContext чтобы определить, следует ли разрешать вызов.
  • Ведение журнала и телеметрия: фильтр может регистрировать сведения и записывать данные о времени и другие статистические данные о вызове метода.
  • Обработка ошибок: фильтр может перехватывать исключения, создаваемые вызовом метода, и преобразовывать его в другое исключение или обрабатывать исключение по мере прохождения фильтра.

Фильтры доступны в двух вариантах:

  • Фильтры входящих вызовов
  • Фильтры исходящих вызовов

Фильтры входящих вызовов выполняются при получении звонка. Фильтры исходящих вызовов выполняются при вызове.

Фильтры входящих вызовов

Фильтры вызовов входящего зерна реализуют IIncomingGrainCallFilter интерфейс, имеющий один метод:

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

Аргумент, IIncomingGrainCallContext переданный методу Invoke , имеет следующую фигуру:

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

Метод IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) должен ожидать или возвращать результат IIncomingGrainCallContext.Invoke() выполнения следующего настроенного фильтра и в конечном итоге сам метод зерна. Свойство Result можно изменить после ожидания Invoke() метода. Свойство ImplementationMethod возвращает MethodInfo класс реализации. Доступ MethodInfo к методу интерфейса можно получить с помощью InterfaceMethod свойства. Фильтры вызовов зерна вызываются для всех вызовов метода к зерне, и это включает вызовы расширений зерна (реализаций IGrainExtension), которые устанавливаются в зерно. Например, расширения зерна используются для реализации маркеров Потоки и отмены. Поэтому следует ожидать, что значение ImplementationMethod не всегда является методом в самом классе зерна.

Настройка фильтров вызовов входящего зерна

Реализации могут быть зарегистрированы IIncomingGrainCallFilter в качестве фильтров с помощью внедрения зависимостей или их можно зарегистрировать в качестве фильтров уровня зерна через зерно, реализующее IIncomingGrainCallFilter напрямую.

Фильтры вызовов silo-wide grain

Делегат можно зарегистрировать в качестве фильтра вызовов с расширением уровня с помощью внедрения зависимостей следующим образом:

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

Аналогичным образом класс можно зарегистрировать в качестве фильтра вызовов зерна с помощью вспомогательного AddIncomingGrainCallFilter метода. Ниже приведен пример фильтра вызова зерна, который регистрирует результаты каждого метода зерна:

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

Затем этот фильтр можно зарегистрировать с помощью AddIncomingGrainCallFilter метода расширения:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Кроме того, фильтр можно зарегистрировать без метода расширения:

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

Фильтры вызовов для каждого зерна

Класс grain может регистрировать себя как фильтр вызовов зерна и фильтровать все вызовы, сделанные к нему, реализуя 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);
}

В приведенном выше примере все вызовы GetFavoriteNumber метода возвращаются 38 вместо 7значения, так как возвращаемое значение было изменено фильтром.

Другой вариант использования фильтров находится в управлении доступом, как в следующем примере:

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

В приведенном выше примере метод может вызываться только в том случае, SpecialAdminOnlyOperation если "isAdmin" задано true значение в .RequestContext Таким образом, фильтры вызовов зерна можно использовать для авторизации. В этом примере вызывающий объект "isAdmin" отвечает за правильность задания значения и правильность выполнения проверки подлинности. Обратите внимание, что [AdminOnly] атрибут указан в методе класса grain. Это связано с тем, что ImplementationMethod свойство возвращает MethodInfo реализацию, а не интерфейс. Фильтр также может проверка InterfaceMethod свойство.

Упорядочение фильтрации вызовов для зерна

Фильтры вызовов зерна соответствуют определенному упорядочению:

  1. IIncomingGrainCallFilter реализации, настроенные в контейнере внедрения зависимостей, в том порядке, в котором они зарегистрированы.
  2. Фильтр уровня зерна, если зерно реализуется IIncomingGrainCallFilter.
  3. Реализация метода зерна или реализация метода расширения зерна.

Каждый вызов IIncomingGrainCallContext.Invoke() инкапсулирует следующий определенный фильтр, чтобы каждый фильтр мог выполнять код до и после следующего фильтра в цепочке и в конечном итоге сам метод зерна.

Фильтры исходящих вызовов

Фильтры исходящих вызовов зерна похожи на входящие фильтры вызовов зерна с основным различием в том, что они вызываются на вызывающем объекте (клиенте), а не на вызываемом объекте (зерне).

Фильтры исходящих вызовов зерна реализуют IOutgoingGrainCallFilter интерфейс, имеющий один метод:

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

Аргумент, IOutgoingGrainCallContext переданный методу Invoke , имеет следующую фигуру:

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

Метод IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) должен ожидать или возвращать результат IOutgoingGrainCallContext.Invoke() выполнения следующего настроенного фильтра и в конечном итоге сам метод зерна. Свойство Result можно изменить после ожидания Invoke() метода. Вызываемый MethodInfo метод интерфейса можно получить с помощью InterfaceMethod свойства. Фильтры исходящих вызовов зерна вызываются для всех вызовов метода к зерне, и это включает вызовы системных методов, сделанных Orleans.

Настройка фильтров исходящих вызовов зерна

Реализации могут быть зарегистрированы как на оси, так и на клиентах IOutgoingGrainCallFilter с помощью внедрения зависимостей.

Делегат можно зарегистрировать в качестве фильтра вызовов следующим образом:

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

В приведенном выше коде builder может быть либо экземпляр ISiloHostBuilder , либо IClientBuilder.

Аналогичным образом класс можно зарегистрировать как фильтр исходящего вызова зерна. Ниже приведен пример фильтра вызова зерна, который регистрирует результаты каждого метода зерна:

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

Затем этот фильтр можно зарегистрировать с помощью AddOutgoingGrainCallFilter метода расширения:

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Кроме того, фильтр можно зарегистрировать без метода расширения:

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

Как и в примере фильтра вызовов делегата, builder может быть экземпляром одного ISiloHostBuilder или.IClientBuilder

Случаи использования

Преобразование исключений

Если исключение, которое было создано с сервера, получает десериализацию на клиенте, иногда может возникать следующее исключение вместо фактического: TypeLoadException: Could not find Whatever.dll.

Это происходит, если сборка, содержащая исключение, недоступна клиенту. Например, предположим, что вы используете Entity Framework в реализации зерна; EntityException затем может быть создано исключение. С другой стороны, клиент не ссылается (и не должен) ссылаться EntityFramework.dll , так как он не знает базовый уровень доступа к данным.

Когда клиент пытается десериализировать EntityExceptionобъект, он завершится ошибкой из-за отсутствия библиотеки DLL; в результате создается исключение, TypeLoadException скрывающее исходный EntityExceptionфайл.

Один может утверждать, что это довольно нормально, так как клиент никогда не будет обрабатывать EntityException; в противном случае он должен будет ссылаться EntityFramework.dll.

Но что делать, если клиент хочет по крайней мере записать исключение? Проблема заключается в том, что исходное сообщение об ошибке потеряно. Одним из способов обойти эту проблему является перехват исключений на стороне сервера и их замена обычными исключениями типа Exception , если тип исключения предположительно неизвестен на стороне клиента.

Однако важно учитывать следующее: мы хотим заменить исключение , если вызывающий клиент является клиентом зерна. Мы не хотим заменить исключение, если вызывающий объект является другим зерном (или Orleans инфраструктурой, которая выполняет вызовы зерна, например, на GrainBasedReminderTable зерно).

На стороне сервера это можно сделать с помощью перехватчика уровня silo:

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

Затем этот фильтр можно зарегистрировать в silo:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Включите фильтр для вызовов, сделанных клиентом, добавив фильтр исходящего вызова:

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

Таким образом клиент сообщает серверу, что он хочет использовать преобразование исключений.

Вызов зерна от перехватчиков

Вызовы зерна из перехватчика можно выполнить путем внедрения 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();
}