Condividi tramite


Filtri di chiamata granulare

I filtri delle chiamate granulari consentono di intercettare le chiamate granulari. I filtri possono eseguire codice sia prima che dopo una chiamata granulare. È possibile installare più filtri contemporaneamente. I filtri sono asincroni e possono modificare RequestContext, argomenti e il valore restituito del metodo richiamato. I filtri possono anche controllare il MethodInfo del metodo richiamato sulla classe grain e possono essere usati per sollevare o gestire le eccezioni.

Di seguito sono riportati alcuni esempi di uso di filtri di chiamata granulari:

  • Autorizzazione: un filtro può esaminare il metodo richiamato e gli argomenti o le informazioni di autorizzazione in RequestContext, per determinare se consentire la continuazione della chiamata.
  • Registrazione/telemetria: un filtro può registrare informazioni e acquisire dati di intervallo e altre statistiche sulla chiamata al metodo.
  • Gestione degli errori: un filtro può intercettare le eccezioni generate da una chiamata al metodo e trasformarle in altre eccezioni o gestire le eccezioni mentre passano attraverso il filtro.

I filtri sono disponibili in due tipi:

  • Filtri delle chiamate in ingresso
  • Filtri delle chiamate in uscita

I filtri di chiamata in ingresso vengono eseguiti quando si riceve una chiamata. I filtri delle chiamate in uscita vengono eseguiti durante l'esecuzione di una chiamata.

Filtri delle chiamate in ingresso

I filtri delle chiamate granulari in ingresso implementano l'interfaccia IIncomingGrainCallFilter , che ha un metodo:

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

L'argomento IIncomingGrainCallContext passato al Invoke metodo ha la forma seguente:

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

Il IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) metodo deve await o restituire il risultato di IIncomingGrainCallContext.Invoke() per eseguire il filtro configurato successivo e infine il metodo granulare stesso. È possibile modificare la Result proprietà dopo aver atteso il Invoke() metodo . La ImplementationMethod proprietà restituisce l'oggetto MethodInfo della classe di implementazione. È possibile accedere all'oggetto MethodInfo del metodo di interfaccia usando la InterfaceMethod proprietà . I filtri delle chiamate granulari vengono chiamati per tutte le chiamate di metodo a un livello di granularità, incluse le chiamate alle estensioni granulari (implementazioni di IGrainExtension) installate nella granularità. Ad esempio, Orleans usa estensioni granulari per implementare flussi e token di annullamento. Pertanto, si prevede che il valore di ImplementationMethod non sia sempre un metodo nella classe granulare stessa.

Configurare i filtri delle chiamate granulari in ingresso

È possibile registrare le implementazioni di IIncomingGrainCallFilter come filtri a livello di silo tramite iniezione di dipendenze o come filtri a livello di grano grazie all'implementazione IIncomingGrainCallFilter diretta di un grano.

Filtri delle chiamate a livello di grano per l'intero silo

È possibile registrare un delegato come filtro di chiamata granulare a livello di silo usando l'inserimento delle dipendenze come segue:

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

Analogamente, è possibile registrare una classe come filtro di chiamata granulare usando il AddIncomingGrainCallFilter metodo helper. Di seguito è riportato un esempio di filtro di chiamata granulare che registra i risultati di ogni metodo granulare:

public class LoggingCallFilter : IIncomingGrainCallFilter
{
    private readonly ILogger<LoggingCallFilter> _logger;

    public LoggingCallFilter(ILogger<LoggingCallFilter> logger)
    {
        _logger = logger;
    }

    public async Task Invoke(IIncomingGrainCallContext context)
    {
        try
        {
            await context.Invoke();

            _logger.LogInformation(
                "{GrainType}.{MethodName} returned value {Result}",
                context.Grain.GetType(),
                context.MethodName,
                context.Result);
        }
        catch (Exception exception)
        {
            _logger.LogError(
                exception,
                "{GrainType}.{MethodName} threw an exception",
                context.Grain.GetType(),
                context.MethodName);

            // If this exception is not re-thrown, it is considered to be
            // handled by this filter.
            throw;
        }
    }
}

Questo filtro può quindi essere registrato usando il AddIncomingGrainCallFilter metodo di estensione:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

In alternativa, il filtro può essere registrato senza il metodo di estensione:

siloHostBuilder.Services
    .AddSingleton<IIncomingGrainCallFilter, LoggingCallFilter>();

Filtri per granularità delle chiamate

Una classe granulare può registrarsi come filtro di chiamata granulare e filtrare tutte le chiamate effettuate implementando IIncomingGrainCallFilter come segue:

public class MyFilteredGrain
    : Grain, IMyFilteredGrain, Orleans.IIncomingGrainCallFilter
{
    public async Task Invoke(Orleans.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);
}

Nell'esempio precedente tutte le chiamate al GetFavoriteNumber metodo restituiscono 38 anziché 7 perché il filtro ha modificato il valore restituito.

Un altro caso d'uso per i filtri è il controllo di accesso, come illustrato in questo esempio:

[AttributeUsage(AttributeTargets.Method)]
public class AdminOnlyAttribute : Attribute { }
public class MyAccessControlledGrain
    : Grain, IMyFilteredGrain, Orleans.IIncomingGrainCallFilter
{
    public Task Invoke(Orleans.IIncomingGrainCallContext context)
    {
        // Check access conditions.
        var isAdminMethod =
            context.ImplementationMethod.GetCustomAttribute<AdminOnlyAttribute>();
        if (isAdminMethod is not null && RequestContext.Get("isAdmin") is not true)
        {
            throw new AccessDeniedException(
                $"Only admins can access {context.ImplementationMethod.Name}!");
        }

        return context.Invoke();
    }

    [AdminOnly]
    public Task<int> GetFavoriteNumber() => Task.FromResult(7);
}

Nell'esempio precedente il SpecialAdminOnlyOperation metodo può essere chiamato solo se "isAdmin" è impostato su true in RequestContext. In questo modo, è possibile usare filtri di chiamata granulari per l'autorizzazione. In questo esempio è responsabilità del chiamante assicurarsi che il "isAdmin" valore sia impostato correttamente e che l'autenticazione venga eseguita correttamente. Si noti che l'attributo [AdminOnly] viene specificato nel metodo della classe grain. Ciò è dovuto al fatto che la ImplementationMethod proprietà restituisce l'oggetto MethodInfo dell'implementazione, non l'interfaccia . Il filtro può anche controllare la InterfaceMethod proprietà .

Ordinamento del filtro delle chiamate granulari

I filtri di chiamata a livello granulare seguono un ordine definito.

  1. IIncomingGrainCallFilter implementazioni configurate nel contenitore di inserimento delle dipendenze, nell'ordine in cui vengono registrate.
  2. Filtro a livello di grana, se la grana implementa IIncomingGrainCallFilter.
  3. Implementazione del metodo granulare o implementazione del metodo di estensione granulare.

Ogni chiamata a IIncomingGrainCallContext.Invoke() incapsula il filtro definito successivo, consentendo a ogni filtro di eseguire codice prima e dopo il filtro successivo nella catena e, infine, il metodo granulare stesso.

Filtri delle chiamate in uscita

I filtri delle chiamate granulari in uscita sono simili ai filtri delle chiamate granulari in ingresso. La differenza principale è che vengono richiamati sul chiamante (client) anziché sul chiamato (grano).

I filtri delle chiamate granulari in uscita implementano l'interfaccia IOutgoingGrainCallFilter , che ha un metodo:

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

L'argomento IOutgoingGrainCallContext passato al Invoke metodo ha la forma seguente:

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

Il IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) metodo deve await o restituire il risultato di IOutgoingGrainCallContext.Invoke() per eseguire il filtro configurato successivo e infine il metodo granulare stesso. È possibile modificare la Result proprietà dopo aver atteso il Invoke() metodo . È possibile accedere all'oggetto MethodInfo del metodo di interfaccia chiamato usando la InterfaceMethod proprietà . I filtri di chiamata ai grain in uscita vengono richiamati per tutte le chiamate ai metodi di un grain, incluse le chiamate ai metodi di sistema eseguite da Orleans.

Configurare i filtri delle chiamate granulari in uscita

È possibile registrare le implementazioni di IOutgoingGrainCallFilter sia sui silo che sui client utilizzando l'iniezione delle dipendenze.

Registrare un delegato come filtro di chiamata come segue:

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

Nel codice precedente, builder può essere un'istanza di ISiloBuilder o IClientBuilder.

Analogamente, è possibile registrare una classe come filtro di chiamata granulare in uscita. Di seguito è riportato un esempio di filtro di chiamata granulare che registra i risultati di ogni metodo granulare:

public class OutgoingLoggingCallFilter : IOutgoingGrainCallFilter
{
    private readonly ILogger<OutgoingLoggingCallFilter> _logger;

    public OutgoingLoggingCallFilter(ILogger<OutgoingLoggingCallFilter> logger)
    {
        _logger = logger;
    }

    public async Task Invoke(IOutgoingGrainCallContext context)
    {
        try
        {
            await context.Invoke();

            _logger.LogInformation(
                "{GrainType}.{MethodName} returned value {Result}",
                context.Grain.GetType(),
                context.MethodName,
                context.Result);
        }
        catch (Exception exception)
        {
            _logger.LogError(
                exception,
                "{GrainType}.{MethodName} threw an exception",
                context.Grain.GetType(),
                context.MethodName);

            // If this exception is not re-thrown, it is considered to be
            // handled by this filter.
            throw;
        }
    }
}

Questo filtro può quindi essere registrato usando il AddOutgoingGrainCallFilter metodo di estensione:

builder.AddOutgoingGrainCallFilter<OutgoingLoggingCallFilter>();

In alternativa, il filtro può essere registrato senza il metodo di estensione:

builder.Services
    .AddSingleton<IOutgoingGrainCallFilter, OutgoingLoggingCallFilter>();

Come per l'esempio di filtro di chiamata delegato, builder può essere un'istanza di ISiloBuilder o IClientBuilder.

Casi d'uso

Conversione delle eccezioni

Quando un'eccezione generata dal server viene deserializzata nel client, a volte si potrebbe ottenere l'eccezione seguente anziché quella effettiva: TypeLoadException: Could not find Whatever.dll.

Ciò si verifica se l'assembly contenente l'eccezione non è disponibile per il client. Si supponga, ad esempio, di usare Entity Framework nelle implementazioni granulari; potrebbe essere generata un'eccezione EntityException . Il client, d'altra parte, non fa riferimento EntityFramework.dll (e non deve) perché non conosce il livello di accesso ai dati sottostante.

Quando il client tenta di deserializzare EntityException, non riesce a causa della DLL mancante. Di conseguenza, viene generato un TypeLoadException, nascondendo il EntityException.

Si potrebbe sostenere che ciò sia accettabile dato che il client non gestirebbe mai il EntityException; altrimenti sarebbe necessario fare riferimento al EntityFramework.dll.

Ma cosa succede se il client vuole almeno registrare l'eccezione? Il problema è che il messaggio di errore originale viene perso. Un modo per risolvere questo problema consiste nell'intercettare le eccezioni lato server e sostituirle con eccezioni semplici di tipo Exception se il tipo di eccezione è presumibilmente sconosciuto sul lato client.

Tuttavia, devi tenere presente un aspetto importante: devi sostituire un'eccezione solo se il chiamante è il client grain. Non si dovrebbe sostituire un'eccezione se il chiamante è un altro grain (o l'infrastruttura Orleans che effettua chiamate a grain, ad esempio, sul grain GrainBasedReminderTable).

Sul lato server è possibile eseguire questa operazione con un intercettore a livello di silo:

public class ExceptionConversionFilter : Orleans.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(Orleans.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));
        }
    }
}

Questo filtro può quindi essere registrato nel silo:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Abilitare il filtro per le chiamate effettuate dal client aggiungendo un filtro di chiamata in uscita:

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

In questo modo, il client indica al server che vuole usare la conversione delle eccezioni.

Richiamare dati da intercettori

È possibile effettuare chiamate granulari da un intercettore inserendo IGrainFactory nella classe dell'intercettore:

public class CustomCallFilter : Orleans.IIncomingGrainCallFilter
{
    private readonly IGrainFactory _grainFactory;

    public CustomCallFilter(IGrainFactory grainFactory)
    {
        _grainFactory = grainFactory;
    }

    public async Task Invoke(Orleans.IIncomingGrainCallContext context)
    {
        // Hook calls to any grain other than ICustomFilterGrain implementations.
        // This avoids potential infinite recursion when calling OnReceivedCall() below.
        if (context.Grain is not ICustomFilterGrain)
        {
            var filterGrain = _grainFactory.GetGrain<ICustomFilterGrain>(
                ((IAddressable)context.Grain).GetPrimaryKeyLong());

            // Perform some grain call here.
            await filterGrain.OnReceivedCall();
        }

        // Continue invoking the call on the target grain.
        await context.Invoke();
    }
}