49  
netcore
Search  
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

Author:
Jeffery Cheng
Source:
Views:
3107

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.

Similar
Apr 28, 2022
Author: Lucas Diogo
Day-to-day tips I use to develop APIs. Every day we need to develop new APIs, whether at work or for study purposes, and some features can help us with the use of good practices in a simple way. And through...
Jul 26
Author: Anton Vasilkovsky
As the pool of technology supporting Web Forms continues to shrink, you want to know if it’s a good idea to modernize your application by integrating ASP.NET Web Forms and ASP.NET MVC. Judging from our vast experience with clients making...
Feb 29
Author: Sharmila Subbiah
Introduction With the release of .NET 8, Microsoft takes a significant stride forward, introducing the native Ahead-of-Time (AOT) compilation for ASP.NET Core. This advancement not only enhances application performance but also simplifies the development process, marking a new era in...
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,...
Send message
Type
Email
Your name
*Message