Compartilhar via


Filtros de chamada de granularidade

Filtros de chamada de granularidade fornecem um meio para interceptar chamadas de granularidade. Os filtros podem executar código antes e depois de uma chamada de granularidade. Vários filtros podem ser instalados simultaneamente. Os filtros são assíncronos e podem modificar argumentos RequestContext e o valor retornado do método que está sendo invocado. Os filtros também podem inspecionar MethodInfo do método que está sendo invocado na classe de granularidade e podem ser usados para gerar ou manipular exceções.

Alguns exemplos de uso de filtros de chamada de granularidade são:

  • Autorização: um filtro pode inspecionar o método que está sendo invocado e os argumentos ou algumas informações de autorização em RequestContext para determinar se a chamada deve ou não continuar.
  • Log/Telemetria: um filtro pode registrar informações e capturar dados de tempo e outras estatísticas sobre invocação de método.
  • Tratamento de erro: um filtro pode interceptar exceções geradas por uma invocação de método e transformá-la em outra exceção ou manipular a exceção à medida que passa pelo filtro.

O filtros vem em duas variantes:

  • Filtros de chamada de entrada
  • Filtros de chamada de saída

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

Filtros de chamada de entrada

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

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

O argumento IIncomingGrainCallContext passado para o método Invoke 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 método IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) deve aguardar ou retornar o resultado de IIncomingGrainCallContext.Invoke() para executar o próximo filtro configurado e, eventualmente, o próprio método de granularidade. A propriedade Result pode ser modificada depois de aguardar o método Invoke(). A propriedade ImplementationMethod retorna a classe MethodInfo de implementação. O MethodInfo do método de interface pode ser acessado usando a propriedade InterfaceMethod. Filtros de chamada de granularidade são chamados para todas as chamadas de método para uma granularidade e isso inclui chamadas para extensões de granularidade (implementações de IGrainExtension) que são instaladas na granularidade. Por exemplo, extensões de granularidade são usadas para implementar fluxos e tokens de cancelamento. Portanto, deve-se esperar que o valor de ImplementationMethod nem sempre seja um método na própria classe de granularidade.

Configurar filtros de chamada de entrada de granularidade

As implementações de IIncomingGrainCallFilter podem ser registradas como filtros em todo o silo por meio da Injeção de Dependência ou podem ser registradas como filtros de nível de granularidade por meio de uma implementação de granularidade IIncomingGrainCallFilter diretamente.

Filtros de chamada de granularidade em todo o silo

Um delegado pode ser registrado como um filtro de chamada de granularidade em todo o silo usando a Injeção de Dependência da seguinte maneira:

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, uma classe pode ser registrada como um filtro de chamada de granularidade usando o método auxiliar AddIncomingGrainCallFilter. Aqui está um exemplo de um filtro de chamada de granularidade que registra os resultados de cada método de granularidade:

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 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 granularidade

Uma classe de granularidade pode ser registrada como um filtro de chamada de granularidade e filtrar todas as chamadas feitas a ela implementando IIncomingGrainCallFilter assim:

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 acima, todas as chamadas para o método GetFavoriteNumber retornarão 38 em vez de 7, porque o valor retornado foi alterado pelo filtro.

Outro caso de uso para filtros está no controle de acesso, como 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 acima, o método SpecialAdminOnlyOperation só pode ser chamado se "isAdmin" estiver definido como true em RequestContext. Dessa forma, filtros de chamada de granularidade podem ser usados 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 atributo [AdminOnly] é especificado no método da classe de granularidade. Isso ocorre porque a propriedade ImplementationMethod retorna MethodInfo da implementação, não a interface. O filtro também pode verificar a propriedade InterfaceMethod.

Ordenação de filtro de chamada de granularidade

Os filtros de chamada de granularidade 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 granularidade, se a granularidade implementar IIncomingGrainCallFilter.
  3. Implementação do método de granularidade ou implementação do método de extensão de granularidade.

Cada chamada para IIncomingGrainCallContext.Invoke() encapsula o próximo filtro definido para que cada filtro tenha a chance de executar o código antes e depois do próximo filtro na cadeia e, eventualmente, o próprio método de granularidade.

Filtros de chamada de saída

Os filtros de chamada de saída de granularidade são semelhantes aos filtros de chamada de entrada de granularidade, com a principal diferença é que eles são invocados no chamador (cliente) em vez do computador chamado (granularidade).

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

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

O argumento IOutgoingGrainCallContext passado para o método Invoke 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 método IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) deve aguardar ou retornar o resultado de IOutgoingGrainCallContext.Invoke() para executar o próximo filtro configurado e, eventualmente, o próprio método de granularidade. A propriedade Result pode ser modificada depois de aguardar o método Invoke(). O MethodInfo do método de interface sendo chamada pode ser acessado usando a propriedade InterfaceMethod. Os filtros de chamada de saída de granularidade são invocados para todas as chamadas de método para uma granularidade e isso inclui chamadas aos métodos do sistema feitos pelo Orleans.

Configurar filtros de chamada de saída de granularidade

As implementações de IOutgoingGrainCallFilter podem ser registradas tanto em silos quanto nos clientes usando a Injeção de Dependência.

Um delegado pode ser registrado como um filtro de chamada da seguinte maneira:

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, uma classe pode ser registrada como um filtro de chamada de saída de granularidade. Aqui está um exemplo de um filtro de chamada de granularidade que registra os resultados de cada método de granularidade:

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

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

Casos de uso

Conversão de exceção

Quando uma exceção gerada do servidor está sendo desserializada no cliente, às vezes você pode obter a exceção a seguir em vez da 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, digamos que você esteja usando o Entity Framework em suas implementações de granularidade; em seguida, um EntityException pode ser lançado. O cliente, por outro lado, não faz referência (e não deve) referenciar EntityFramework.dll, pois não conhece a camada de acesso aos dados subjacente.

Quando o cliente tentar desserializar o EntityException, ele falhará devido à DLL ausente; como consequência, um TypeLoadException é lançado ocultando o original EntityException.

Pode-se argumentar que isso está ok, pois o cliente nunca trataria o EntityException; caso contrário, ele teria que referenciar 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 é interceptar exceções do lado do servidor e substituí-las por exceções simples de tipo Exception se o tipo de exceção for presumivelmente desconhecido no lado do cliente.

No entanto, há uma coisa importante que temos que ter em mente: só queremos substituir uma exceção se o chamador for o cliente de granularidade . Não queremos substituir uma exceção se o chamador é outra granularidade (ou a infraestrutura do Orleans que também está fazendo chamadas de granularidade; por exemplo, na granularidade GrainBasedReminderTable).

No lado do servidor, isso pode ser feito com um interceptador no nível do 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));
        }
    }
}

Esse filtro pode ser registrado no silo:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Habilite o filtro para chamadas feitas pelo cliente adicionando um filtro de chamada 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.

Chamar granularidades de interceptadores

É possível fazer chamadas de granularidades de um interceptador injetando IGrainFactory na classe do interceptador:

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