Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jun 27, 2022

Implement gRPC global exception handler in ASP.NET

Автор:
Jeffery Cheng
Источник:
Просмотров:
3097

This example shows you the gRpc global exception handler in the unary server handle in gRpc.

In microservice, we have two ways to integrate with other internal services.

The first way is the Request-Response pattern, which is the most famous.

The Request-Response pattern advantage is that the client could immediately get the response from the other inner services, whether it is view data or data operation result.

The second way is the Message pattern; the producer queues data to the queue (like Kafka), and the consumer will receive the data from the queue.

In system design, what time uses the Request-Response pattern, and what time uses the Message pattern?

In the following few articles, I will give more explanation about this.

Our internal system uses gRpc to integrate with other interior services to implement the Request-Response pattern.

The question in this pattern is how do we know whether the response is success or failure?

The response in this pattern has two failure situations; one is the internet error another is a business error.

To distinguish the response, we formulate a format in the API response.

The response key has code, message, and data; the following JSON is an example.

{
    "code":"0000",
    "message":"success",
    "data":
    {
        "order_no": "2022010100011"
    }
}
  • Code: Indicates whether your operation is successful; if not, it will give the corresponding error code.
  • Message: Some error messages that API wants to tell you.
  • Data: You wanted data from the service.

Global exception handler in web API

If we want to handle the global exception in web API, we write a middleware, catch the specific domain exception, and rewrite the response body.

First, we create a domain exception class.

The domain exception has two properties, which are code and message.

public class DomainException : Exception
{
    public string Code { get; set; }
    public string Message { get; set; }
    public DomainException(string code,string message)
    {
        Code = code;
        Message = message;
    }
}

And if our domain logic occurs exceptions, like order not found or store not found, we throw the domain exception.

var order = await _orderRepository.GetAsync(string orderNo);

if (order is null)
   throw new DomainException("1001","Order not found, please check your order number.");

The last step is to create a middleware to handle the domain exception.

The global exception handler middleware will catch the domain exception and rewrite the response body to our specification format with the code and message in the domain exception.

public class WebApiGlobalExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<WebApiGlobalExceptionHandlerMiddleware> _logger;

    public WebApiGlobalExceptionHandlerMiddleware(RequestDelegate next,
        ILogger<WebApiGlobalExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (DomainException de)
        {
            // we don't log any messages, because we know this is a business error.
            var responseVm = new ResponseViewModel
            {
                Code = de.Code,
                Message = de.Message
            };
            
            await RewriteBodyAsync(httpContext.Response,responseVm);
        }
        catch (Exception e)
        {
            _logger.LogError(e,e.Message);
            
            var responseVm = new ResponseViewModel
            {
                Code = "99999",
                Message = "Server error"
            };
            
            await RewriteBodyAsync(httpContext.Response,responseVm);
        }
    }

    private async Task RewriteBodyAsync(HttpResponse httpResponse,ResponseViewModel responseViewModel)
    {
        httpResponse.ContentType = "application/json; charset=utf-8";
        httpResponse.StatusCode = (int)HttpStatusCode.OK;

        await httpResponse.WriteAsJsonAsync(responseViewModel);
    }
}

Is the solution the same available on gRpc?

The gRpc request goes through the global exception handler middleware as well.

Let's see what happens if we don't exclude the gRpc request from the global exception handler middleware.

Oops, we got a status code 2 UNKNOWN response.

The solution works on web API, completely unavailable on gRpc.

Global exception handler in gRpc

We rewrite the JSON body that can fit the format in web API, as you can see.

But in gRpc, we couldn't use the middleware to handle exceptions as web API does.

So we need to use another way to handle this.

Let's use Grpc.Core.Interceptors to handle exceptions.

Create a Grpc global exception handler interceptor as following code.

Register the interceptor to the gRpc server pipeline.

public class GrpcGlobalExceptionHandlerInterceptor : Interceptor
{
    private readonly ILogger<GrpcGlobalExceptionHandlerInterceptor> _logger;
    
    public GrpcGlobalExceptionHandlerInterceptor(ILogger<GrpcGlobalExceptionHandlerInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await base.UnaryServerHandler(request, context, continuation);
        }
        catch (DomainException de)
        {
            var responseVm = new ResponseViewModel
            {
                Code = de.Code,
                Message = de.Message
            };
            
            return MapResponse<TRequest,TResponse>(responseVm);
        }
        catch (Exception e)
        {
            _logger.LogError(e,e.Message);
            
            var responseVm = new ResponseViewModel
            {
                Code = "99999",
                Message = "Server error"
            };
            
            return MapResponse<TRequest,TResponse>(responseVm);
        }
    }

    private TResponse MapResponse<TRequest, TResponse>(ResponseViewModel responseViewModel)
    {
        var concreteResponse = Activator.CreateInstance<TResponse>();

        concreteResponse?.GetType().GetProperty(nameof(responseViewModel.Code))?.SetValue(concreteResponse,responseViewModel.Code);

        concreteResponse?.GetType().GetProperty(nameof(responseViewModel.Message))?.SetValue(concreteResponse,responseViewModel.Message);
        
        return concreteResponse;
    }
}

 

var builder = WebApplication.CreateBuilder(args);

// add interceptor to gRpc server pipeline
builder.Services.AddGrpc(c=>c.Interceptors.Add<GrpcGlobalExceptionHandleInterceptor>());

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();

app.MapGet("/",
    () =>
        "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();

The gRpc response body is a concrete type; we could not rewrite the JSON body as web API does when we catch the exception.

We must return a specificated class to the response.

So we use reflection to map to the generic type.

Is work if the global exception handler middleware and gRpc global exception handler interceptor exist simultaneously?

The answer is yes.

But only one way could successfully catch the exception.

The cause is the order in middleware and interceptor.

The web API request will not go through the gRpc interceptor.

So the middleware will catch those exceptions thrown from the web API requests.

And the gRpc request does not only go through the middleware but also the interceptor.

The order of request is the middleware first and the gRpc interceptor last.

So the interceptor will catch the exceptions thrown from the gRpc requests instead of throwing them forward to middleware.

In the following few articles, I will illustrate the order in middleware and interceptor in ASP.NET.

Conclusion

This example shows you the gRpc global exception handler in the unary server handle in gRpc.

If you are using client streaming or server streaming, you need in additional handle those methods.

Just override the other server handle methods.

I hope this solution can solve your problems.

Похожее
Jul 22
Author: Ankit Sahu
Introduction In this article, we are going to discuss What Entity Framework is and How we can implement it in the .Net 8 project. This is a continuation of part 1, so if you are new to this article, please...
Jun 26, 2021
SignalR is a popular software library that allows you send server code to client-side applications, like a React application. This tutorial will show you how to install SignalR on both your ASP.NET Core and React Typescript application. Creating your Applications...
Mar 29
Author: Colton
These interview questions range from basic to advanced .Net Core, and will assist you in preparing for interviews, revising quickly, and strengthening your technical skills. According to the Stackoverflow Survey 2020,.NET and.NET Core are the second and third most popular...
Sep 14, 2023
Author: Mickvdv
In the world of modern software architecture, reliable communication between different components or microservices is crucial. This is where RabbitMQ, a queue based message broker, can play a vital role. RabbitMQ is a popular choice for implementing message queuing systems,...
Написать сообщение
Тип
Почта
Имя
*Сообщение