Поиск  
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
Источник:
Просмотров:
3103

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.

Похожее
Oct 24, 2020
Author: Sandeep Singh Shekhawat
Introduction The Onion Architecture term was coined by Jeffrey Palermo in 2008. This architecture provides a better way to build applications for better testability, maintainability, and dependability on the infrastructures like databases and services. This architecture's main aim is to...
Aug 15, 2023
Whether you have an app with just a few users or millions of users per day, like Agoda, improving the user experience by optimizing application performance is always crucial. In the case of very high-traffic websites in the cloud, this...
Apr 25
Author: FeatBit
Follow me on Twitter, happy to take your suggestions on topics or improvements. Introduction Many third-party feature flagging services are available, or your internal team could develop its own feature flag service. For those who prefer standardization, following the OpenFeature...
Dec 23, 2023
Author: Juldhais Hengkyawan
This article will teach us how to retrieve the client’s IP address and location information in ASP.NET Core web development. Retrieve the client IP from HttpContext In ASP.NET Core, you can easily get the client IP address from the HttpContext...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS