곡물 호출 필터는 곡물 호출을 가로채는 방법을 제공합니다. 필터는 곡물 호출 전후에 코드를 실행할 수 있습니다. 여러 필터를 동시에 설치할 수 있습니다. 필터는 비동기적이며 RequestContext, 인수 및 호출되는 메서드의 반환 값을 수정할 수 있습니다. 필터는 그레인 클래스에서 MethodInfo 호출되는 메서드를 검사할 수도 있으며 예외를 throw하거나 처리하는 데 사용할 수 있습니다.
곡물 호출 필터의 몇 가지 예제 사용은 다음과 같습니다.
-
권한 부여: 필터는 호출되는 메서드와 인수 또는 권한 부여 정보를
RequestContext검사하여 호출을 계속할 수 있는지 여부를 결정할 수 있습니다. - 로깅/원격 분석: 필터는 정보를 기록하고 타이밍 데이터 및 메서드 호출에 대한 기타 통계를 캡처할 수 있습니다.
- 오류 처리: 필터는 메서드 호출에서 throw된 예외를 가로채 다른 예외로 변환하거나 필터를 통과할 때 예외를 처리할 수 있습니다.
필터는 다음 두 가지 유형으로 제공됩니다.
- 들어오는 호출 필터
- 전화 발신 필터
수신 호출 필터는 전화를 받을 때 실행됩니다. 발신 호출 필터는 호출을 할 때 실행됩니다.
들어오는 호출 필터
들어오는 데이터 호출 필터는 하나의 메서드를 가진 IIncomingGrainCallFilter 인터페이스를 구현합니다.
public interface IIncomingGrainCallFilter
{
Task Invoke(IIncomingGrainCallContext context);
}
IIncomingGrainCallContext 메서드에 전달된 인수의 Invoke 모양은 다음과 같습니다.
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)의 결과를 반환하거나 awaitIIncomingGrainCallContext.Invoke()해야 합니다. 메서드 Result를 대기한 후에 Invoke() 속성을 수정할 수 있습니다. 이 속성은 구현 클래스의 ImplementationMethod을 반환합니다.
MethodInfo 속성을 사용하여 인터페이스 메서드의 InterfaceMethod에 액세스할 수 있습니다. Grain 호출 필터는 그레인에 대한 모든 메서드 호출에 적용되며, 그레인에 설치된 IGrainExtension의 구현과 같은 그레인 확장에 대한 호출도 포함됩니다. 예를 들어 Orleans은 스트림과 취소 토큰을 구현하기 위해 grain 확장을 사용합니다. 따라서 값 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 도우미 메서드를 사용하여 클래스를 그레인 호출 필터로 등록할 수 있습니다. 다음은 모든 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;
}
}
}
그런 다음 확장 메서드를 사용하여 이 필터를 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);
}
앞의 예제에서, SpecialAdminOnlyOperation가 "isAdmin"에서 true로 설정된 경우에만 RequestContext 메서드를 호출할 수 있습니다. 이러한 방식으로 권한 부여를 위해 곡물 호출 필터를 사용할 수 있습니다. 이 예제에서는 값이 올바르게 설정되고 인증이 올바르게 수행되도록 하는 "isAdmin" 것은 호출자의 책임입니다. 특성은 [AdminOnly] grain 클래스 메서드에 지정됩니다. 이는 ImplementationMethod 속성이 인터페이스가 아닌 구현의 MethodInfo을 반환하기 때문입니다. 필터는 InterfaceMethod 속성을 확인할 수도 있습니다.
Grain 호출 필터 순서 지정
그레인 호출 필터는 정의된 순서를 따릅니다.
-
IIncomingGrainCallFilter종속성 주입 컨테이너에 구성된 구현은 등록된 순서대로 구성됩니다. - 그레인이
IIncomingGrainCallFilter를 구현하는 경우, 그레인 수준의 필터입니다. - Grain 메서드 구현 또는 grain 확장 메서드 구현입니다.
각 호출은 다음 정의된 필터를 IIncomingGrainCallContext.Invoke() 캡슐화하여 각 필터가 체인의 다음 필터 전후에 코드를 실행할 수 있도록 하고, 결국에는 그레인 메서드 자체를 실행할 수 있습니다.
전화 발신 필터
나가는 곡물 호출 필터는 들어오는 곡물 호출 필터와 유사합니다. 주요 차이점은 호출 수신자(그레인)가 아닌 호출자(클라이언트)에서 호출된다는 것입니다.
나가는 곡물 호출 필터는 IOutgoingGrainCallFilter 인터페이스를 구현하며, 이 인터페이스에는 한 가지 메서드가 있습니다.
public interface IOutgoingGrainCallFilter
{
Task Invoke(IOutgoingGrainCallContext context);
}
IOutgoingGrainCallContext 메서드에 전달된 인수의 Invoke 모양은 다음과 같습니다.
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)의 결과를 반환하거나 awaitIOutgoingGrainCallContext.Invoke()해야 합니다. 메서드 Result를 대기한 후에 Invoke() 속성을 수정할 수 있습니다.
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는 ISiloHostBuilder 또는 IClientBuilder의 인스턴스일 수 있습니다.
마찬가지로, 그레인 호출 필터로 클래스를 등록할 수 있습니다. 다음은 모든 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;
}
}
}
그런 다음 확장 메서드를 사용하여 이 필터를 AddOutgoingGrainCallFilter 등록할 수 있습니다.
builder.AddOutgoingGrainCallFilter<LoggingCallFilter>();
또는 확장 메서드 없이 필터를 등록할 수 있습니다.
builder.ConfigureServices(
services => services.AddSingleton<IOutgoingGrainCallFilter, LoggingCallFilter>());
대리자 호출 필터 예제 builder 와 마찬가지로 둘 중 하나 ISiloHostBuilder 또는 IClientBuilder의 인스턴스일 수 있습니다.
사용 사례
예외 변환
서버에서 throw된 예외가 클라이언트에서 역직렬화되면 실제 예외 대신 다음과 같은 예외가 발생할 수 있습니다. 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();
}
.NET