조직 호출 필터

조직 호출 필터는 조직 호출을 가로챌 수 있는 수단을 제공합니다. 필터는 조직 호출 전후에 코드를 실행할 수 있습니다. 여러 필터를 동시에 설치할 수 있습니다. 필터는 비동기적이며 RequestContext, 인수 및 호출되는 메서드의 반환 값을 수정할 수 있습니다. 필터는 조직 클래스에서 호출되는 메서드의 MethodInfo를 검사할 수도 있으며 예외를 throw하거나 처리하는 데 사용할 수 있습니다.

조직 호출 필터의 몇 가지 사용 예는 다음과 같습니다.

  • 권한 부여: 필터는 호출되는 메서드와 RequestContext의 인수 또는 일부 권한 부여 정보를 검사하여 호출을 계속 진행할지 여부를 확인할 수 있습니다.
  • 로깅/원격 분석: 필터는 정보를 기록하고 타이밍 데이터 및 메서드 호출에 대한 기타 통계를 캡처할 수 있습니다.
  • 오류 처리: 필터는 메서드 호출에서 throw된 예외를 가로채서 다른 예외로 변환하거나 필터를 통과할 때 예외를 처리할 수 있습니다.

필터는 두 가지 버전으로 제공됩니다.

  • 수신 호출 필터
  • 발신 호출 필터

수신 호출 필터는 전화를 받을 때 실행됩니다. 발신 호출 필터는 전화를 걸 때 실행됩니다.

수신 호출 필터

들어오는 조직 호출 필터는 하나의 메서드가 있는 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()의 결과를 기다리거나 반환해야 합니다. Result 속성은 Invoke() 메서드를 기다린 후 수정할 수 있습니다. ImplementationMethod 속성은 구현 클래스의 MethodInfo를 반환합니다. 인터페이스 메서드의 MethodInfoInterfaceMethod 속성을 사용하여 액세스할 수 있습니다. 조직 호출 필터는 조직에 대한 모든 메서드 호출에 대해 호출되며, 여기에는 조직에 설치된 조직 확장(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 메서드에 대한 모든 호출은 7 대신 38를 반환합니다.

필터에 대한 또 다른 사용 사례는 다음 예제와 같이 액세스 제어에 있습니다.

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

위의 예제에서 SpecialAdminOnlyOperation 메서드는 RequestContext에서 "isAdmin"true로 설정된 경우에만 호출할 수 있습니다. 이러한 방식으로 권한 부여를 위해 조직 호출 필터를 사용할 수 있습니다. 이 예제에서 "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()의 결과를 기다리거나 반환해야 합니다. Result 속성은 Invoke() 메서드를 기다린 후 수정할 수 있습니다. 호출되는 인터페이스 메서드의 MethodInfoInterfaceMethod 속성을 사용하여 액세스할 수 있습니다. 나가는 노이즈 호출 필터는 노이즈에 대한 모든 메서드 호출에 대해 호출되며 여기에는 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;
    }
});

위의 코드에서 builderISiloHostBuilder 또는 IClientBuilder의 인스턴스일 수 있습니다.

마찬가지로, 클래스를 나가는 조직 호출 필터로 등록할 수 있습니다. 다음은 모든 조직 메서드의 결과를 기록하는 조직 호출 필터의 예입니다.

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

대리자 호출 필터 예제와 마찬가지로 builderISiloHostBuilder 또는 IClientBuilder의 인스턴스일 수 있습니다.

사용 사례

예외 변환

서버에서 throw된 예외가 클라이언트에서 역직렬화되는 경우 실제 예외 대신 TypeLoadException: Could not find Whatever.dll. 예외가 발생할 수 있습니다.

이 문제는 예외가 포함된 어셈블리를 클라이언트에서 사용할 수 없는 경우에 발생합니다. 예를 들어, 조직 구현에서 Entity Framework를 사용한다고 가정합니다. 그런 다음, EntityException을 throw할 수 있습니다. 반면 클라이언트는 기본 데이터 액세스 계층을 알지 못하기 때문에 EntityFramework.dll을 참조하지 않습니다.

클라이언트가 EntityException을 역직렬화하려고 하면 누락된 DLL로 인해 실패합니다. 결과적으로 TypeLoadException은 원래 EntityException을 숨기고 throw됩니다.

클라이언트가 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();
}