31  
webapi
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:
3106

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
Nov 24, 2022
Author: Chandan Das
Blazor and Razor have a healthy following in the web UI dev community, especially among developers who primarily work within .NET Core. However, the confusion between these two can be off-putting for some developers, especially beginners. This post discusses the...
Jul 26, 2023
Author: Jason Watmore
Built with .NET 7.0, Dapper 2.0 and PostgreSQL In this tutorial we'll show how to build a .NET 7.0 (ASP.NET Core) API with Dapper and PostgreSQL that supports CRUD operations. Tutorial contents Example API overview Tools required to run the...
Jul 29
Author: Rick Strahl
Over the last few years, Markdown has become a ubiquitous text-entry model for HTML text. It's creeping up in more and more places and has become a standard for documents that are shared and edited for documentation purposes on the...
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...
Send message
Type
Email
Your name
*Message