Dela via


Filter för kornanrop

Filter för kornanrop är ett sätt att fånga upp kornanrop. Filter kan köra kod både före och efter ett kornigt anrop. Flera filter kan installeras samtidigt. Filter är asynkrona och kan ändra RequestContext, argument och returvärdet för metoden som anropas. Filter kan också kontrollera vilken MethodInfo metod som anropas i kornklassen och kan användas för att generera eller hantera undantag.

Några exempel på användningar av filter för kornanrop är:

  • Auktorisering: ett filter kan kontrollera vilken metod som anropas och argumenten eller viss auktoriseringsinformation i RequestContext för att avgöra om anropet ska kunna fortsätta eller inte.
  • Loggning/telemetri: ett filter kan logga information och samla in tidsdata och annan statistik om metodanrop.
  • Felhantering: ett filter kan fånga upp undantag som genereras av ett metodanrop och omvandla det till ett annat undantag eller hantera undantaget när det passerar genom filtret.

Filter finns i två varianter:

  • Filter för inkommande samtal
  • Filter för utgående samtal

Inkommande samtalsfilter körs när du tar emot ett samtal. Utgående samtalsfilter körs när ett anrop görs.

Filter för inkommande samtal

Filter för inkommande kornanrop implementerar IIncomingGrainCallFilter gränssnittet, som har en metod:

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

Argumentet IIncomingGrainCallContext som skickas Invoke till metoden har följande form:

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

Metoden IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) måste invänta eller returnera resultatet av IIncomingGrainCallContext.Invoke() för att köra nästa konfigurerade filter och slutligen själva kornmetoden. Egenskapen Result kan ändras efter att du har väntat på Invoke() metoden. Egenskapen ImplementationMethod returnerar MethodInfo implementeringsklassen. Du MethodInfo kan komma åt gränssnittsmetoden med hjälp av egenskapen InterfaceMethod . Filter för kornanrop anropas för alla metodanrop till ett korn och detta inkluderar anrop till korntillägg (implementeringar av IGrainExtension) som är installerade i kornigheten. Till exempel används korntillägg för att implementera Flöden- och annulleringstoken. Därför bör det förväntas att värdet för ImplementationMethod inte alltid är en metod i själva kornklassen.

Konfigurera filter för inkommande kornsamtal

Implementeringar av IIncomingGrainCallFilter kan antingen registreras som siloomfattande filter via beroendeinmatning eller så kan de registreras som filter på kornnivå via ett korn som implementerar IIncomingGrainCallFilter direkt.

Filter för Silo-wide grain-anrop

Ett ombud kan registreras som ett siloomfattande anropsfilter med beroendeinmatning så här:

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

På samma sätt kan en klass registreras som ett kornigt anropsfilter med hjälpmetoden AddIncomingGrainCallFilter . Här är ett exempel på ett kornigt anropsfilter som loggar resultatet av varje kornmetod:

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

Det här filtret kan sedan registreras med hjälp av AddIncomingGrainCallFilter tilläggsmetoden:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

Alternativt kan filtret registreras utan tilläggsmetoden:

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

Anropsfilter per kornnivå

En kornklass kan registrera sig som ett kornanropsfilter och filtrera alla anrop som görs till den genom att implementera IIncomingGrainCallFilter så här:

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

I exemplet ovan returneras 38 alla anrop till GetFavoriteNumber metoden i stället för 7, eftersom returvärdet har ändrats av filtret.

Ett annat användningsfall för filter finns i åtkomstkontroll, som i det här exemplet:

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

I exemplet ovan SpecialAdminOnlyOperation kan metoden bara anropas om "isAdmin" den är inställd trueRequestContexti . På så sätt kan filter för kornanrop användas för auktorisering. I det här exemplet är det anroparens ansvar att se till att "isAdmin" värdet har angetts korrekt och att autentiseringen utförs korrekt. Observera att [AdminOnly] attributet anges på metoden för kornklass. Det beror på att ImplementationMethod egenskapen returnerar MethodInfo implementeringen, inte gränssnittet. Filtret kan också kontrollera egenskapen InterfaceMethod .

Kornig anropsfilterordning

Filter för kornanrop följer en definierad ordning:

  1. IIncomingGrainCallFilter implementeringar som konfigurerats i containern för beroendeinmatning i den ordning de registreras.
  2. Kornnivåfilter, om kornet implementerar IIncomingGrainCallFilter.
  3. Implementering av kornmetod eller metodimplementering av korntillägg.

Varje anrop för att IIncomingGrainCallContext.Invoke() kapsla in nästa definierade filter så att varje filter har en chans att köra kod före och efter nästa filter i kedjan och slutligen själva kornmetoden.

Filter för utgående samtal

Filter för utgående kornanrop liknar inkommande kornanropsfilter, där den största skillnaden är att de anropas på anroparen (klienten) i stället för anroparen (korn).

Filter för utgående kornanrop implementerar IOutgoingGrainCallFilter gränssnittet, som har en metod:

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

Argumentet IOutgoingGrainCallContext som skickas Invoke till metoden har följande form:

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

Metoden IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) måste invänta eller returnera resultatet av IOutgoingGrainCallContext.Invoke() för att köra nästa konfigurerade filter och slutligen själva kornmetoden. Egenskapen Result kan ändras efter att du har väntat på Invoke() metoden. Den MethodInfo gränssnittsmetod som anropas kan nås med hjälp av InterfaceMethod egenskapen . Filter för utgående kornanrop anropas för alla metodanrop till ett korn och detta inkluderar anrop till systemmetoder som görs av Orleans.

Konfigurera filter för utgående kornsamtal

Implementeringar av kan antingen registreras på både silor och klienter med hjälp av IOutgoingGrainCallFilter beroendeinmatning.

Ett ombud kan registreras som ett anropsfilter så här:

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

I koden ovan builder kan vara antingen en instans av ISiloHostBuilder eller IClientBuilder.

På samma sätt kan en klass registreras som ett utgående kornanropsfilter. Här är ett exempel på ett kornigt anropsfilter som loggar resultatet av varje kornmetod:

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

Det här filtret kan sedan registreras med hjälp av AddOutgoingGrainCallFilter tilläggsmetoden:

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

Alternativt kan filtret registreras utan tilläggsmetoden:

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

Precis som med exemplet builder med anropsfilter för ombud kan vara en instans av antingen ISiloHostBuilder eller IClientBuilder.

Användningsfall

Undantagskonvertering

När ett undantag som har genererats från servern deserialiseras på klienten kan du ibland få följande undantag i stället för det faktiska: TypeLoadException: Could not find Whatever.dll.

Detta inträffar om sammansättningen som innehåller undantaget inte är tillgänglig för klienten. Anta till exempel att du använder Entity Framework i dina kornimplementeringar. sedan kan en EntityException kastas. Klienten å andra sidan refererar inte till (och bör inte) EntityFramework.dll eftersom den inte känner till det underliggande dataåtkomstskiktet.

När klienten försöker deserialisera EntityExceptionkommer den att misslyckas på grund av den saknade DLL:en, vilket innebär att en TypeLoadException genereras och döljer den ursprungliga EntityException.

Man kan hävda att detta är ganska okej eftersom klienten aldrig skulle hantera EntityException; annars skulle det behöva referera EntityFramework.dlltill .

Men vad händer om klienten åtminstone vill logga undantaget? Problemet är att det ursprungliga felmeddelandet går förlorat. Ett sätt att kringgå det här problemet är att fånga upp undantag på serversidan och ersätta dem med vanliga undantag av typen Exception om undantagstypen förmodligen är okänd på klientsidan.

Det finns dock en viktig sak som vi måste tänka på: vi vill bara ersätta ett undantag om anroparen är kornklienten. Vi vill inte ersätta ett undantag om anroparen är ett annat korn (eller infrastrukturen Orleans som gör kornanrop också, t.ex. på kornet GrainBasedReminderTable ).

På serversidan kan detta göras med en silonivåavlyssnare:

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

Det här filtret kan sedan registreras på silon:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

Aktivera filtret för anrop som görs av klienten genom att lägga till ett utgående anropsfilter:

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

På så sätt meddelar klienten servern att den vill använda undantagskonvertering.

Anropa korn från interceptorer

Det är möjligt att göra kornanrop från en interceptor genom att IGrainFactory mata in i interceptor-klassen:

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