粒紋呼叫篩選

粒紋呼叫篩選可提供攔截粒紋呼叫的方法。 篩選可以在粒紋呼叫前後執行程式碼。 您可以同時安裝多個篩選。 篩選為非同步進行,且可以修改 RequestContext、引數,以及所叫用方法的傳回值。 篩選也可以檢查在粒紋類別上所叫用方法的 MethodInfo,並可用來擲回或處理例外狀況。

粒紋呼叫篩選的一些範例用法如下:

  • 授權:篩選可以檢查所叫用的方法,以及 RequestContext 中的引數或某些授權資訊,以判斷是否要允許呼叫繼續。
  • 記錄/遙測:篩選可以記錄資訊及擷取計時資料,以及有關方法調用的其他統計資料。
  • 錯誤處理:篩選可以攔截方法調用所擲回的例外狀況,並將其轉換成另一個例外狀況,或在通過篩選時處理例外狀況。

篩選分為兩種類別:

  • 來電篩選
  • 去電篩選

接收通話時,會執行來電篩選。 撥打電話時,會執行去電篩選。

來電篩選

傳入的粒紋呼叫篩選會實作 IIncomingGrainCallFilter 介面,其有一個方法:

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

傳遞至 Invoke 方法的 IIncomingGrainCallContext 引數具有下列圖形:

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

IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext) 方法必須等候或傳回 IIncomingGrainCallContext.Invoke() 的結果,才能執行下一個設定的篩選準則,最後執行粒紋方法本身。 在等候 Invoke() 方法之後,即可修改 Result 屬性。 ImplementationMethod 屬性傳回實作類別的 MethodInfo。 介面方法的 MethodInfo 可以使用 InterfaceMethod 屬性進行存取。 針對粒紋的所有方法呼叫都會呼叫粒紋呼叫篩選,包括對粒紋延伸模組的呼叫 (IGrainExtension 實作),其會安裝在粒紋中。 例如,粒紋延伸模組會用來實作串流和取消權杖。 因此,應該預期 ImplementationMethod 的值不一定是粒紋類別本身中的方法。

設定傳入的粒紋通話篩選

實作 IIncomingGrainCallFilter 可以透過相依性插入註冊為全定址接收器篩選,或者也可以透過直接實作 IIncomingGrainCallFilter 的粒紋註冊為粒紋層級篩選。

全定址接收器的粒紋呼叫篩選器

您可以使用相依性插入將委派註冊為全定址接收器的粒紋呼叫篩選,如下所示:

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

同樣地,您也可以使用 AddIncomingGrainCallFilter 協助程式方法將類別註冊為粒紋呼叫篩選。 以下是記錄每個粒紋方法結果的粒紋呼叫篩選範例:

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

接著,您可以使用 AddIncomingGrainCallFilter 擴充方法註冊此篩選:

siloHostBuilder.AddIncomingGrainCallFilter<LoggingCallFilter>();

或者,不需要擴充方法即可註冊篩選:

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

每個粒紋的粒紋呼叫篩選

粒紋類別可以將其本身註冊為粒紋呼叫篩選,並藉由實作 IIncomingGrainCallFilter 來篩選對其所做的任何呼叫,如下所示:

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

在上述範例中,GetFavoriteNumber 方法的所有呼叫都會傳回 38,而不是 7,因為篩選已改變傳回值。

篩選的另一個使用案例為存取控制,如下列範例所示:

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

在上述範例中,僅在 RequestContext 中將 "isAdmin" 設定為 true 時,才能呼叫 SpecialAdminOnlyOperation 方法。 如此一來,就可以使用粒紋呼叫篩選進行授權。 在此範例中,呼叫者必須負責確保 "isAdmin" 值已正確設定,且驗證已正確執行。 請注意,會在粒紋類別方法上指定 [AdminOnly] 屬性。 這是因為 ImplementationMethod 屬性會傳回實作的 MethodInfo,而不是介面。 篩選也可以檢查 InterfaceMethod 屬性。

粒紋呼叫篩選排序

粒紋呼叫篩選會遵循定義的排序:

  1. 在相依性插入容器中設定的 IIncomingGrainCallFilter 實作,依據其所註冊的順序。
  2. 如果粒紋實作 IIncomingGrainCallFilter,則為粒紋層級篩選。
  3. 粒紋方法實作或粒紋擴充方法實作。

每個 IIncomingGrainCallContext.Invoke() 的呼叫都會封裝下一個定義的篩選,讓每個篩選有機會在鏈結中的下一個篩選前後執行程式碼,最後執行粒紋方法本身。

去電篩選

傳出粒紋呼叫篩選類似於傳入粒紋呼叫篩選,主要差異在於其是在呼叫者 (用戶端) 叫用,而非在被呼叫者 (粒紋) 叫用。

傳出的粒紋呼叫篩選會實作 IOutgoingGrainCallFilter 介面,其有一個方法:

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

傳遞至 Invoke 方法的 IOutgoingGrainCallContext 引數具有下列圖形:

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

IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext) 方法必須等候或傳回 IOutgoingGrainCallContext.Invoke() 的結果,才能執行下一個設定的篩選準則,最後執行粒紋方法本身。 在等候 Invoke() 方法之後,即可修改 Result 屬性。 所呼叫介面方法的 MethodInfo 可以使用 InterfaceMethod 屬性進行存取。 所有對粒紋的方法呼叫都會叫用傳出粒紋呼叫篩選,這包括對 Orleans 所發出系統方法的呼叫。

設定傳出粒紋通話篩選

實作 IOutgoingGrainCallFilter 可以在使用相依性插入的定址接收器和用戶端上進行註冊。

委派可以註冊為呼叫篩選,如下所示:

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

在上述程式碼中,builder 可以是 ISiloHostBuilderIClientBuilder 的執行個體。

同樣地,類別也可以註冊為傳出粒紋呼叫篩選。 以下是記錄每個粒紋方法結果的粒紋呼叫篩選範例:

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

接著,您可以使用 AddOutgoingGrainCallFilter 擴充方法註冊此篩選:

builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();

或者,不需要擴充方法即可註冊篩選:

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

如同委派呼叫篩選範例,builder 可以是 ISiloHostBuilderIClientBuilder 的執行個體。

使用案例

例外狀況轉換

當從伺服器擲回的例外狀況在用戶端上還原序列化時,您有時可能會收到下列例外狀況,而不是實際例外狀況:TypeLoadException: Could not find Whatever.dll.

如果包含例外狀況的組件無法供用戶端使用,就會發生這種情況。 例如,假設您在粒紋實作中使用 Entity Framework;則可能會擲回 EntityException。 另一方面,用戶端不會 (且不應該) 參考 EntityFramework.dll,因為其不知道基礎資料存取層。

當用戶端嘗試還原序列化 EntityException 時,其將會因為遺漏 DLL 而失敗;因此,會擲回 TypeLoadException,並隱藏原始的 EntityException

可能會有人認為這沒關係,因為用戶端永遠不會處理 EntityException;否則,其必須參考 EntityFramework.dll

但是,如果用戶端至少需要記錄例外狀況,該怎麼辦? 問題在於原始錯誤訊息遺失。 解決此問題的其中一種方法,是攔截伺服器端例外狀況,並在用戶端上假設例外狀況類型未知時,以 Exception 類型的純文字例外狀況加以取代。

不過,我們必須記住一個重要事項:僅在呼叫端為粒紋用戶端時,才需要取代例外狀況。 如果呼叫端是另一個粒紋 (或是也會進行粒紋呼叫的 Orleans 基礎結構;例如,在 GrainBasedReminderTable 粒紋上) 我們也不需要取代例外狀況。

在伺服器端,這可以使用定址接收器層級攔截器來完成:

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

接著,可以在定址接收器上註冊此篩選:

siloHostBuilder.AddIncomingGrainCallFilter<ExceptionConversionFilter>();

藉由新增去電篩選來啟用用戶端所發出呼叫的篩選:

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

如此一來,用戶端就會告訴伺服器,其需要使用例外狀況轉換。

從攔截器呼叫粒紋

您可以將 IGrainFactory 插入攔截器類別,從攔截器進行粒紋呼叫:

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