Partage via


Filtres d'appels de grain

Les filtres d'appels de grain offrent un moyen d'intercepter ces appels. Les filtres peuvent exécuter du code avant et après un appel de grain. Vous pouvez installer plusieurs filtres simultanément. Les filtres sont asynchrones et peuvent modifier RequestContext, arguments et valeur de retour de la méthode appelée. Les filtres peuvent également inspecter la MethodInfo méthode appelée sur la classe de grain et peuvent être utilisés pour lever ou gérer des exceptions.

Voici quelques exemples d’utilisations de filtres d’appel pour traitement granulaire :

  • Autorisation : un filtre peut inspecter la méthode appelée et les arguments, ou les informations d’autorisation dans le RequestContext, pour déterminer s’il faut autoriser l’appel à continuer.
  • Journalisation/télémétrie : un filtre peut consigner des informations et capturer des données temporelles et d’autres statistiques sur l’appel de méthode.
  • Gestion des erreurs : un filtre peut intercepter les exceptions levées par un appel de méthode et les transformer en autres exceptions ou gérer les exceptions lors de leur passage dans le filtre.

Les filtres sont fournis dans deux types :

  • Filtres d’appels entrants
  • Filtres d’appels sortants

Les filtres d’appel entrants sont exécutés lors de la réception d’un appel. Les filtres d’appels sortants sont exécutés lors de l’exécution d’un appel.

Filtres d’appels entrants

Les filtres d'appels de grain entrants implémentent l’interface IIncomingGrainCallFilter, qui a une méthode :

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

L’argument IIncomingGrainCallContext passé à la Invoke méthode a la forme suivante :

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

La méthode IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) doit await ou retourner le résultat de IIncomingGrainCallContext.Invoke() afin d'exécuter le filtre configuré suivant et, finalement, la méthode grain elle-même. Vous pouvez modifier la Result propriété après avoir attendu la Invoke() méthode. La ImplementationMethod propriété retourne la MethodInfo classe d’implémentation. Vous pouvez accéder à la MethodInfo méthode d’interface à l’aide de la InterfaceMethod propriété. Les filtres d’appels de grain sont appelés pour tous les appels de méthode à un grain, y compris les appels aux extensions de grain (implémentations de IGrainExtension) installés dans le grain. Par exemple, Orleans utilise des extensions de grain pour implémenter des flux et des jetons d’annulation. Sachez que la valeur de ImplementationMethod n'est pas toujours une méthode dans la classe grain elle-même.

Configurer les filtres d’appels de grain entrants

Vous pouvez inscrire des implémentations de IIncomingGrainCallFilter soit en tant que filtres à l'échelle du silo via l'injection de dépendances, soit en tant que filtres au niveau du grain en faisant implémenter directement IIncomingGrainCallFilter par un grain.

Filtres d’appel de grain à l’échelle du silo

Vous pouvez inscrire un délégué en tant que filtre d’appel de grain à l’échelle du silo à l’aide de l’injection de dépendances comme suit :

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

De même, vous pouvez enregistrer une classe en tant que filtre d’appel de grain à l’aide de la méthode utilitaire AddIncomingGrainCallFilter. Voici un exemple de filtre d’appel de grain qui enregistre les résultats de chaque méthode de grain :

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

Ce filtre peut ensuite être inscrit à l’aide de la méthode d’extension AddIncomingGrainCallFilter :

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Vous pouvez également inscrire le filtre sans la méthode d’extension :

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

Filtres d'appel par grain individuel

Une classe de grain peut s’inscrire en tant que filtre d’appel de grain et filtrer les appels qui lui sont adressés, en implémentant IIncomingGrainCallFilter comme suit :

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

Dans l'exemple précédent, tous les appels à la méthode GetFavoriteNumber retournent 38 au lieu de 7 parce que le filtre a modifié la valeur de retour.

Un autre cas d’usage pour les filtres est le contrôle d’accès, comme illustré dans cet exemple :

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

Dans l’exemple précédent, la méthode SpecialAdminOnlyOperation ne peut être appelée que si "isAdmin" est défini à true dans RequestContext. De cette façon, vous pouvez utiliser des filtres d'appels granulaires pour l’autorisation. Dans cet exemple, il incombe à l’appelant de s’assurer que la "isAdmin" valeur est correctement définie et que l’authentification est effectuée correctement. Notez que l’attribut [AdminOnly] est spécifié sur la méthode de classe de grain. Cela est dû au fait que la ImplementationMethod propriété retourne l’implémentation MethodInfo , et non l’interface. Le filtre peut également vérifier la InterfaceMethod propriété.

Ordre des filtres d’appels de grain

Les filtres d’appel de grain suivent un ordre défini :

  1. IIncomingGrainCallFilter implémentations configurées dans le conteneur d’injection de dépendances, dans l’ordre dans lequel elles sont inscrites.
  2. Filtre au niveau du grain, si le grain implémente IIncomingGrainCallFilter.
  3. Implémentation de la méthode graine ou implémentation de la méthode d’extension de grain.

Chaque appel à IIncomingGrainCallContext.Invoke() encapsule le filtre défini suivant, ce qui permet à chaque filtre d’exécuter du code avant et après le filtre suivant dans la chaîne et, finalement, la méthode grain elle-même.

Filtres d’appels sortants

Les filtres d’appel de grain sortant sont similaires aux filtres d’appels de grain entrants. La principale différence est qu'elles sont invoquées sur l'appelant (client) plutôt que sur l'appelé (cible).

Les filtres d’appels de grain sortant implémentent l’interface IOutgoingGrainCallFilter , qui a une méthode :

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

L’argument IOutgoingGrainCallContext passé à la Invoke méthode a la forme suivante :

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

La méthode IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) doit await ou retourner le résultat de IOutgoingGrainCallContext.Invoke() afin d'exécuter le filtre configuré suivant et, finalement, la méthode grain elle-même. Vous pouvez modifier la Result propriété après avoir attendu la Invoke() méthode. Vous pouvez accéder à la MethodInfo méthode d’interface appelée à l’aide de la InterfaceMethod propriété. Les filtres d’appels de grain sortants sont appelés pour tous les appels de méthode à un grain, y compris les appels aux méthodes système effectuées par Orleans.

Configurer les filtres d’appels de grain sortants

Vous pouvez inscrire des implémentations de IOutgoingGrainCallFilter sur les silos et les clients à l’aide de l’injection de dépendances.

Inscrivez un délégué en tant que filtre d’appel comme suit :

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

Dans le code ci-dessus, builder peut être une instance de ISiloHostBuilder ou IClientBuilder.

De même, vous pouvez inscrire une classe en tant que filtre d’appel de grain sortant. Voici un exemple de filtre d’appel de grain qui enregistre les résultats de chaque méthode de grain :

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

Ce filtre peut ensuite être inscrit à l’aide de la méthode d’extension AddOutgoingGrainCallFilter :

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Vous pouvez également inscrire le filtre sans la méthode d’extension :

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

Comme avec l’exemple du filtre d’appel délégué, builder peut être une instance de ISiloHostBuilder ou de IClientBuilder.

Cas d’utilisation

Conversion d’exception

Lorsqu’une exception levée par le serveur est désérialisée sur le client, vous pouvez parfois obtenir l’exception suivante au lieu de l’exception d'origine : TypeLoadException: Could not find Whatever.dll.

Cela se produit si l’assembly contenant l’exception n’est pas disponible pour le client. Par exemple, supposons que vous utilisez Entity Framework dans vos implémentations de grain ; un EntityException peut être levé. Le client ne fait pas référence (et ne doit pas non plus) à EntityFramework.dll, puisqu'il ne connaît pas la couche d’accès aux données sous-jacente.

Lorsque le client tente de désérialiser le EntityException, il échoue en raison de la DLL manquante. Par conséquent, une TypeLoadException est levée, masquant le EntityException d'origine.

On peut affirmer que cela est acceptable puisque le client ne gérerait jamais le EntityException; sinon, il aurait besoin de référencer EntityFramework.dll.

Mais que se passe-t-il si le client souhaite au moins enregistrer l’exception ? Le problème est que le message d’erreur d’origine est perdu. Une façon de contourner ce problème consiste à intercepter les exceptions côté serveur et à les remplacer par des exceptions simples de type si le type Exception d’exception est probablement inconnu côté client.

Toutefois, gardez à l’esprit une chose importante : vous ne souhaitez remplacer une exception que si l’appelant est le client grain. Vous ne souhaitez pas remplacer une exception si c'est un autre grain qui appelle (ou si l’infrastructure Orleans effectue des appels de grain, par exemple sur un grain GrainBasedReminderTable).

Côté serveur, vous pouvez le faire avec un intercepteur au niveau du 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));
        }
    }
}

Ce filtre peut ensuite être inscrit sur le silo :

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Activez le filtre pour les appels effectués par le client en ajoutant un filtre d’appel sortant :

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

De cette façon, le client indique au serveur qu’il souhaite utiliser la conversion d’exception.

Appeler des grains à partir d’intercepteurs

Vous pouvez effectuer des appels de grain à partir d’un intercepteur en injectant IGrainFactory dans la classe d’intercepteur :

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