Перехватчики¶
Перехватчик - это своего рода промежуточное ПО, выполняемое для каждого gRPC-запроса, и только для gRPC-запроса. Промежуточное ПО ASP.NET Core выполняется до выполнения перехватчиков. Во время работы перехватчиков сообщение ещё не сериализовано, тогда как промежуточное ПО ASP.NET Core получает доступ к потоку байтов.
Перехватчик — это класс, унаследованный от абстрактного класса Interceptor, и может быть применён как глобально, так и точечно, для конкретного сервиса. Можно определить несколько перехватчиков, и они будут выполнены в том порядке, в котором были объявлены.
Класс Interceptor предоставляет четыре доступных для переопределения виртуальных метода:
UnaryServerHandlerClientStreamingServerHandlerServerStreamingServerHandlerDuplexStreamingServerHandler
Отметим, что перехватчики поддерживают внедрение зависимостей, и поэтому в них можно внедрить ILogger. По умолчанию перехватчики имеют время жизни Per request. 1
Создадим перехватчик, заменяющий возникающее исключение на RpcException:
using CountryService.Web.Interceptors.Helpers;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace CountryService.Web.Interceptors;
public class ExceptionInterceptor : Interceptor
{
private readonly ILogger<ExceptionInterceptor> _logger;
private readonly Guid _correlationId;
public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
{
_logger = logger;
_correlationId = Guid.NewGuid();
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (Exception e)
{
throw e.Handle(context, _logger, _correlationId);
}
}
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(requestStream, context);
}
catch (Exception e)
{
throw e.Handle(context, _logger, _correlationId);
}
}
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
try
{
await continuation(request, responseStream, context);
}
catch (Exception e)
{
throw e.Handle(context, _logger, _correlationId);
}
}
public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
DuplexStreamingServerMethod<TRequest, TResponse> continuation)
{
try
{
await continuation(requestStream, responseStream, context);
}
catch (Exception e)
{
throw e.Handle(context, _logger, _correlationId);
}
}
}
Здесь метод
Handle - это метод расширения для исключения, как раз и преобразующий возникающее исключение в соответствующее RpcException. Вот его код:using Grpc.Core;
using Microsoft.Data.SqlClient;
namespace CountryService.Web.Interceptors.Helpers;
public static class ExceptionHelpers
{
public static RpcException Handle(
this Exception exception,
ServerCallContext context,
ILogger logger,
Guid correlationId) =>
exception switch
{
TimeoutException timeoutException => HandleTimeoutException(timeoutException, context, logger, correlationId),
SqlException sqlException => HandleSqlException(sqlException, context, logger, correlationId),
RpcException rpcException => HandleRpcException(rpcException, context, logger, correlationId),
_ => HandleDefault(exception, context, logger, correlationId)
};
private static RpcException HandleTimeoutException(TimeoutException exception, ServerCallContext context, ILogger logger, Guid correlationId)
{
logger.LogError(exception, "CorrelationId: {correlationId} - A timeout occurred", correlationId);
var status = new Status(StatusCode.Internal, "An external resource did not answer within the time limit");
return new RpcException(status, CreateTrailers(correlationId));
}
private static RpcException HandleSqlException(SqlException exception, ServerCallContext context, ILogger logger, Guid correlationId)
{
logger.LogError(exception, "CorrelationId: {correlationId} - A timeout occurred", correlationId);
var status = exception.Number == -2
? new Status(StatusCode.DeadlineExceeded, "SQL timeout")
: new Status(StatusCode.Internal, "SQL Error");
return new RpcException(status, CreateTrailers(correlationId));
}
private static RpcException HandleRpcException(RpcException exception, ServerCallContext context, ILogger logger, Guid correlationId)
{
logger.LogError(exception, "CorrelationId: {correlationId} - An error occurred", correlationId);
var trailers = exception.Trailers;
// см. сноску 2
var newTrailers = CreateTrailers(correlationId);
foreach (var entry in trailers)
{
newTrailers.Add(entry);
}
return new RpcException(new Status(exception.Status.StatusCode, exception.Status.Detail), newTrailers);
}
private static RpcException HandleDefault(Exception exception, ServerCallContext context, ILogger logger, Guid correlationId)
{
logger.LogError(exception, "CorrelationId: {correlationId} - An error occurred", correlationId);
return new RpcException(new Status(StatusCode.Internal, exception.Message), CreateTrailers(correlationId));
}
private static Metadata CreateTrailers(Guid correlationId)
{
var trailers = new Metadata();
trailers.Add("CorrelationId", correlationId.ToString());
return trailers;
}
}
Здесь мы реализовали специальные методы для исключений с типами
TimeoutException, SqlException и RpcException — на случай, если наше приложение будет подключаться к другому gRPC-сервису в качестве клиента2.
От меня
В книге класс назван ExceptionHelpers. Я обычно называю такие классы ExceptionExtensions, потому что он содержит методы расширения. Однако, сейчас будем следовать написанному в книге.
Осталось добавить наш перехватчик в общую коллекцию перехватчиков:
builder.Services.AddGrpc(options =>
{
// другие опции...
options.Interceptors.Add<ExceptionInterceptor>(); //регистрируем наш перехватчик
});
-
Время жизни можно изменить — см. справку Microsoft ↩
-
В книге, а также блоге автора приведён такой код:
private static RpcException HandleRpcException<T>(RpcException exception, ILogger<T> logger, Guid correlationId) { logger.LogError(exception, $"CorrelationId: {correlationId} - An error occurred"); var trailers = exception.Trailers; trailers.Add(CreateTrailers(correlationId)[0]); return new RpcException(new Status(exception.StatusCode, exception.Message), trailers); }
Но при попытке его использовать я получал исключениеSystem.InvalidOperationException: Object is read onlyв строкеtrailers.Add(CreateTrailers(correlationId)[0]);. Не знаю, проверял ли автор свой код, но я выложу ту версию, которая сработала для меня. ↩
Дата создания : 5 апреля 2023 г.