Partilhar via


Filtros de chamada de grãos

Os filtros de chamada de grãos fornecem um meio para intercetar chamadas de grãos. Os filtros podem executar código antes e depois de uma chamada de grão. Vários filtros podem ser instalados 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 o MethodInfo método que está sendo invocado na classe de grão e podem ser usados para lançar ou manipular exceções.

Alguns exemplos de usos de filtros de chamada de grão 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 no RequestContext para determinar se deve ou não permitir que a chamada prossiga.
  • 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á-la em outra exceção ou manipular a exceção à medida que ela passa pelo filtro.

Os filtros vêm em dois sabores:

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

Configurar filtros de chamada de grão de entrada

As implementações de podem ser registradas como filtros em todo o silo via injeção de dependência ou podem ser registradas como filtros de nível de grão por meio de IIncomingGrainCallFilter uma implementação IIncomingGrainCallFilter de grão diretamente.

Filtros de chamada de grãos em todo o silo

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

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 AddIncomingGrainCallFilter extensão:

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 grão pode registrar-se como um filtro de chamada de grão 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 GetFavoriteNumber método retornarão 38 em vez de , porque o valor de 7retorno foi alterado pelo filtro.

Outro caso de uso para filtros é 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 SpecialAdminOnlyOperation método só pode ser chamado se "isAdmin" estiver definido como true no RequestContext. Desta forma, filtros de chamada de grãos podem ser usados para autorização. Neste exemplo, é responsabilidade do chamador garantir que o valor seja definido corretamente e que a "isAdmin" 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 de grãos

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 para que cada filtro tenha a chance de executar o código antes e depois do próximo filtro na cadeia e, eventualmente, do próprio método grain.

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, com a principal diferença sendo que eles são invocados no chamador (cliente) em vez do destinatário (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 aguardar ou retornar o resultado de para executar o próximo filtro configurado e, eventualmente, o próprio método de IOutgoingGrainCallContext.Invoke() grão. A Result propriedade pode ser modificada depois de aguardar o Invoke() método. O MethodInfo método de interface que está sendo chamado pode ser acessado 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 e isso inclui chamadas para métodos de sistema feitas por Orleans.

Configurar filtros de chamada de grão de saída

As implementações de podem ser registradas em silos e clientes usando a injeção de IOutgoingGrainCallFilter dependência.

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

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 AddOutgoingGrainCallFilter extensão:

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 delegado, builder pode ser uma instância de um ou ISiloHostBuilder IClientBuilder.

Casos de utilização

Conversão de exceção

Quando uma exceção que foi lançada do servidor está sendo desserializada no cliente, às vezes você pode obter a seguinte exceção 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 grão; então um EntityException pode ser jogado. 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 aos dados subjacente.

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

Pode-se argumentar que isso é muito bom, já que o cliente nunca lidaria com o EntityExceptionproblema, caso contrário, teria que fazer referência 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, há uma coisa importante que temos que ter em mente: só queremos substituir uma exceção se o chamador for o cliente do grão. Não queremos substituir uma exceção se o chamador for outro grão (ou a Orleans infraestrutura que está fazendo chamadas de grãos também; por exemplo, no GrainBasedReminderTable grão).

No lado do servidor, isso pode ser feito com um intercetador de nível 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

É possível fazer chamadas de grão de um intercetador injetando IGrainFactory na classe de intercetador:

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