Partilhar via


Filtros de chamada de grão

Os filtros de chamada de grãos fornecem uma maneira de intercetar chamadas de grãos. Os filtros podem executar código antes e depois de uma chamada de grão. Você pode instalar vários filtros simultaneamente. Os filtros são assíncronos e podem modificar RequestContext, argumentos e o valor de retorno do método que está sendo invocado. Os filtros também podem inspecionar MethodInfo do método que está a ser invocado na classe 'grain' e podem ser usados para lançar ou lidar com exceções.

Alguns exemplos de aplicação de filtros de chamada de granularidade são:

  • Autorização: Um filtro pode inspecionar o método que está a ser invocado e os argumentos, ou as informações de autorização em RequestContext, para determinar se a chamada deve continuar.
  • Logging/Telemetria: Um filtro pode registrar informações e capturar dados de temporização e outras estatísticas sobre a invocação do método.
  • Tratamento de erros: um filtro pode intercetar exceções lançadas por uma invocação de método e transformá-las em outras exceções ou manipular as exceções à medida que passam pelo filtro.

Os filtros são de dois tipos:

  • Filtros de chamadas recebidas
  • Filtros de chamadas de saída

Os filtros de chamada recebida são executados ao receber uma chamada. Os filtros de chamada de saída são executados ao fazer uma chamada.

Filtros de chamadas recebidas

Os filtros de chamada de grão de entrada implementam a IIncomingGrainCallFilter interface, que tem um método:

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

O IIncomingGrainCallContext argumento passado para o Invoke método tem a seguinte forma:

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

O IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) método deve await ou retornar o resultado de IIncomingGrainCallContext.Invoke() para executar o próximo filtro configurado e, eventualmente, o próprio método de grão. Você pode modificar a Result propriedade depois de aguardar o Invoke() método. A ImplementationMethod propriedade retorna o MethodInfo da classe de implementação. Você pode acessar o MethodInfo método de interface usando a InterfaceMethod propriedade. Os filtros de chamada de grão são acionados para todas as chamadas de método a um grão, incluindo chamadas para extensões de grão (implementações de IGrainExtension) que estão instaladas no grão. Por exemplo, Orleans usa grain extensions para implementar fluxos e tokens de cancelamento. Portanto, espere que o valor de ImplementationMethod nem sempre seja um método na própria classe grain.

Configurar filtros de chamadas de grão recebidas

Você pode registrar implementações de IIncomingGrainCallFilter como filtros abrangentes de silo via injeção de dependência ou como filtros ao nível do grão fazendo o próprio grão implementar diretamente IIncomingGrainCallFilter.

Filtros de chamada de grãos abrangendo todo o silo

Você pode registrar um delegado como um filtro de chamada de grão em todo o silo usando a injeção de dependência da seguinte forma:

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

Da mesma forma, pode-se registar uma classe como filtro de chamadas de grain utilizando o método auxiliar AddIncomingGrainCallFilter. Aqui está um exemplo de um filtro de chamada granular que regista os resultados de cada método granular.

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

Esse filtro pode então ser registrado usando o método de extensão AddIncomingGrainCallFilter.

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Alternativamente, o filtro pode ser registrado sem o método de extensão:

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

Filtros de chamada por grão

Uma classe de grain pode registar-se como um filtro de chamadas de grain e filtrar todas as chamadas feitas para si ao implementar IIncomingGrainCallFilter da seguinte forma:

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

No exemplo anterior, todas as chamadas para o método GetFavoriteNumber retornam 38 em vez de 7 porque o filtro alterou o valor de retorno.

Outro caso de uso para filtros é o controle de acesso, como mostrado neste exemplo:

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

No exemplo anterior, o SpecialAdminOnlyOperation método só pode ser chamado se "isAdmin" estiver definido como true no RequestContext. Assim, pode-se usar filtros de chamada de "grain" para autorização. Neste exemplo, é responsabilidade do chamador garantir que o valor "isAdmin" seja definido corretamente e que a autenticação seja executada corretamente. Observe que o [AdminOnly] atributo é especificado no método de classe de grão. Isso ocorre porque a ImplementationMethod propriedade retorna o MethodInfo da implementação, não a interface. O filtro também pode verificar a InterfaceMethod propriedade.

Ordenação do filtro de chamadas Grain

Os filtros de chamada de grãos seguem uma ordem definida:

  1. IIncomingGrainCallFilter implementações configuradas no contêiner de injeção de dependência, na ordem em que são registradas.
  2. Filtro de nível de grão, se o grão implementa IIncomingGrainCallFilter.
  3. Implementação do método de grãos ou implementação do método de extensão de grãos.

Cada chamada para IIncomingGrainCallContext.Invoke() encapsular o próximo filtro definido, permitindo a cada filtro uma chance de executar código antes e depois do próximo filtro na cadeia e, eventualmente, o próprio método de grão.

Filtros de chamadas de saída

Os filtros de chamada de grão de saída são semelhantes aos filtros de chamada de grão de entrada. A principal diferença é que eles são invocados no chamador (cliente) em vez do callee (grão).

Os filtros de chamada de grão de saída implementam a IOutgoingGrainCallFilter interface, que tem um método:

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

O IOutgoingGrainCallContext argumento passado para o Invoke método tem a seguinte forma:

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

O IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) método deve await ou retornar o resultado de IOutgoingGrainCallContext.Invoke() para executar o próximo filtro configurado e, eventualmente, o próprio método de grão. Você pode modificar a Result propriedade depois de aguardar o Invoke() método. Você pode acessar o MethodInfo método de interface que está sendo chamado usando a InterfaceMethod propriedade. Os filtros de chamada de grão de saída são invocados para todas as chamadas de método para um grão, incluindo chamadas para métodos de sistema feitas por Orleans.

Configurar filtros de chamada de saída para grain

Você pode registrar implementações de IOutgoingGrainCallFilter tanto em silos quanto em clientes usando a injeção de dependência.

Registe um delegado como um filtro de chamada desta forma:

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

No código acima, builder pode ser uma instância de ISiloHostBuilder ou IClientBuilder.

Da mesma forma, você pode registrar uma classe como um filtro de chamada de grão de saída. Aqui está um exemplo de um filtro de chamada granular que regista os resultados de cada método granular.

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

Esse filtro pode então ser registrado usando o método de extensão AddOutgoingGrainCallFilter.

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Alternativamente, o filtro pode ser registrado sem o método de extensão:

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

Tal como acontece com o exemplo de filtro de chamada delegate, builder pode ser uma instância de ISiloHostBuilder ou de IClientBuilder.

Casos de uso

Conversão de exceção

Quando uma exceção lançada pelo servidor é desserializada no cliente, poder-se-á, por vezes, obter a seguinte exceção em vez da exceção real: TypeLoadException: Could not find Whatever.dll.

Isso acontece se o assembly que contém a exceção não estiver disponível para o cliente. Por exemplo, suponhamos que se use o Entity Framework nas implementações de grain; uma EntityException pode ser lançada. O cliente, por outro lado, não faz (e não deve) fazer referência EntityFramework.dll , uma vez que não conhece a camada de acesso a dados subjacente.

Quando o cliente tenta desserializar o EntityException, ele falha devido à DLL ausente. Como consequência, um TypeLoadException é lançado, escondendo o original EntityException.

Pode-se argumentar que isso é aceitável, uma vez que o cliente nunca lidaria com o EntityException; caso contrário, precisaria fazer referência a EntityFramework.dll.

Mas e se o cliente quiser pelo menos registrar a exceção? O problema é que a mensagem de erro original é perdida. Uma maneira de contornar esse problema é intercetar exceções do lado do servidor e substituí-las por exceções simples do tipo Exception se o tipo de exceção for presumivelmente desconhecido no lado do cliente.

No entanto, tenha uma coisa importante em mente: você só quer substituir uma exceção se o chamador for o cliente grain. Você não deseja substituir uma exceção se o chamador for outro componente (ou a infraestrutura Orleans que realiza chamadas a componentes, por exemplo, no componente GrainBasedReminderTable).

No lado do servidor, é possível fazer isso com um intercetador a nível de 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));
        }
    }
}

Este filtro pode então ser registado no silo:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Habilite o filtro para chamadas feitas pelo cliente adicionando um filtro de chamadas de saída:

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

Dessa forma, o cliente informa ao servidor que deseja usar a conversão de exceção.

Grãos de chamada de intercetadores

Pode efetuar chamadas "grain" a partir de um interceptor ao injetar IGrainFactory na classe interceptor:

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