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

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.

Похожее
Dec 21, 2023
Author: Jeremy Wells
Introduction and prerequisites This post is part of an ongoing series where we build a “walking skeleton” application using ASP.NET Core and Angular as well as other technologies for deployment and testing. By now, our application is a minimally functional...
Apr 24, 2022
Author: HungryWolf
What is MediatR? Why do we need it? And How to use it? Mediator Pattern - The mediator pattern ensures that objects do not interact directly instead of through a mediator. It reduces coupling between objects which makes it easy...
Aug 8
Author: Davit Asryan
The growth of the internet has made instant communication technology more important than ever, especially for the Internet of Things (IoT). With so many devices like smart home gadgets and industrial sensors needing to talk to each other smoothly, having...
Sep 10, 2023
Author: Sriram Kumar Mannava
In a situation where we need to modify our API’s structure or functionality while ensuring that existing API clients remain unaffected, the solution is versioning. We can designate our current APIs as the older version and introduce all intended changes...
Написать сообщение
Тип
Почта
Имя
*Сообщение