GRpc exception handling Filter

Keywords: SQL github

Global error handling server

Microsoft has implemented it Interceptors They are similar to Filter or Middlewares stay ASP.NET MVC core or web API, they can be used for global exception handling, logging, validation, etc.
This is the server-side Interceptor's own implementation. Continuation is a Task that must wait. Then, if any exception is thrown, it can be controlled according to the exception obtained RpcException And associated StatusCode

using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace DemoGrpc.Web.Logging
{
    public class LoggerInterceptor : Interceptor
    {
        private readonly ILogger<LoggerInterceptor> _logger;

        public LoggerInterceptor(ILogger<LoggerInterceptor> logger)
        {
            _logger = logger;
        }

        public async override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            LogCall(context);
            try
            {
                return await continuation(request, context);
            }
            catch (SqlException e)
            {
                _logger.LogError(e, $"An SQL error occured when calling {context.Method}");
                Status status;

                if (e.Number == -2)
                {
                    status = new Status(StatusCode.DeadlineExceeded, "SQL timeout");
                }
                else
                {
                    status = new Status(StatusCode.Internal, "SQL error");
                }
                throw new RpcException(status);
            }
            catch (Exception e)
            {
                _logger.LogError(e, $"An error occured when calling {context.Method}");
                throw new RpcException(Status.DefaultCancelled, e.Message);
            }
            
        }

        private void LogCall(ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();
            _logger.LogDebug($"Starting call. Request: {httpContext.Request.Path}");
        }
    }
}

The registration method is as follows

using DemoGrpc.Web.Logging;
using DemoGrpc.Web.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DemoGrpc.Web
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            //Register GRpc global exception capture
            services.AddGrpc(options =>
            {
                options.Interceptors.Add<LoggerInterceptor>();
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<CountryGrpcService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("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");
                });
            });
        }
    }
}

The second method can also catch GRpc exceptions, but the writing method is relatively rough. Not recommended

using AutoMapper;
using DemoGrpc.Domain.Entities;
using DemoGrpc.Protobufs;
using DempGrpc.Services.Interfaces;
using Grpc.Core;
using System;
using System.Threading.Tasks;

public class CountryGrpcService : CountryService.CountryServiceBase
{
    private readonly ICountryService _countryService;
    private readonly IMapper _mapper;

    public CountryGrpcService(ICountryService countryService, IMapper mapper)
    {
        _countryService = countryService;
        _mapper = mapper;
    }

    public override async Task<CountriesReply> GetAll(EmptyRequest request, ServerCallContext context)
    {
        try
        {
            var countries = await _countryService.GetAsync();
            return _mapper.Map<CountriesReply>(countries);
        }
        catch (Exception e)
        {
            throw new RpcException(Status.DefaultCancelled, e.Message);
        }
    }
}

Rpc exception information is described below

A common title A common title
Aborted Operations are aborted, usually due to concurrency problems, such as sequencer check failure, transaction abort, etc.
AlreadyExists Some of the entities you are trying to create (for example, files or directories) already exist.
Cancelled The operation is cancelled (usually by the caller).
DataLoss Irrecoverable data loss or corruption.
DeadlineExceeded The deadline has expired before the operation is completed. For operations that change the state of the system, this error is returned even if the operation has been successfully completed. For example, a successful response from the server may be delayed to an expiration date.
FailedPrecondition The operation was rejected because the system is not in the state required to perform the operation. For example, the directory to be deleted may be non empty, an rmdir operation is applied to a non directory, and so on.
Internal Internal error. Some invariants representing the underlying system expectations are broken.
InvalidArgument The client specified an invalid parameter. Note that this is not the same as failed_ The priority is different. INVALID_ARGUMENT represents parameters that are independent of the system state (such as a malformed file name).
NotFound Some requested entities (for example, files or directories) were not found.
OK Successful return
OutOfRange The operation attempt exceeded the valid range. For example, find or read the front end of a file.
PermissionDenied The caller does not have permission to perform the specified operation. PERMISSION_DENIED cannot be used for rejections due to exhaustion of certain resources (for those errors, resource should be used_ EXHAUSTED). Permission cannot be used if the caller is not recognized_ Denied (for those errors, use unauthorized).
ResourceExhausted Some resources have been exhausted, either the quota per user or the entire file system is out of space.
Unauthenticated Not certified / authorized
Unavailable The service is currently unavailable. This is likely to be a temporary situation that can be corrected by stepping back and trying again. Note that it is not always safe to try a non idempotent operation again.
Unimplemented The operation is not implemented or supported / enabled in this service.
Unknown Unknown error. An example that might return this error is if the status value received from another address space belongs to an unknown error space in that address space. If the api does not return enough error information, it may be converted to this error.

Specific address: https://grpc.github.io/grpc/csharp/api/Grpc.Core.StatusCode.html

RpcException has corresponding overloads: the details are as follows. You can customize the information returned by the exception

Global error handling client

The client can also handle errors through interceptors (implement client events, such as AsyncUnaryCall)

using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace ConsoleAppGRPC.Logging
{
    public class LoggerInterceptor : Interceptor
    {
        private readonly ILogger<LoggerInterceptor> _logger;

        public LoggerInterceptor(ILogger<LoggerInterceptor> logger)
        {
            _logger = logger;
        }

        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            LogCall(context.Method);

            var call = continuation(request, context);

            return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
        }

        private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
        {
            try
            {
                var response = await t;
                _logger.LogDebug($"Response received: {response}");
                return response;
            }
            catch (RpcException ex)
            {
                _logger.LogError($"Call error: {ex.Message}");
                return default;
            }
        }

        private void LogCall<TRequest, TResponse>(Method<TRequest, TResponse> method) where TRequest : class where TResponse : class
        {
            _logger.LogDebug($"Starting call. Type: {method.Type}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
        }
    }
}

usage method:

using DemoGrpc.Domain.Entities;
using DemoGrpc.Protobufs;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
using static DemoGrpc.Protobufs.CountryService;
using Microsoft.Extensions.Logging;
using ConsoleAppGRPC.Logging;

namespace ConsoleAppGRPC
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var services = new ServiceCollection();
            services.AddScoped<LoggerInterceptor>();
            services.AddLogging(logging =>
            {
                logging.AddConsole();
                logging.SetMinimumLevel(LogLevel.Debug);
            });

            services.AddGrpcClient<CountryServiceClient>(o =>
            {
                o.Address = new Uri("https://localhost:5001");
            }).AddInterceptor<LoggerInterceptor>();

            var provider = services.BuildServiceProvider();
            var client = provider.GetRequiredService<CountryServiceClient>();
            var logger = provider.GetRequiredService<ILogger<Program>>();

            var countries = (await client.GetAllAsync(new EmptyRequest()))?.Countries.Select(x => new Country
            {
                CountryId = x.Id,
                Description = x.Description,
                CountryName = x.Name
            }).ToList();

            if (countries != null)
            {
                logger.LogInformation("Found countries");
                countries.ForEach(x => Console.WriteLine($"Found country {x.CountryName} ({x.CountryId}) {x.Description}"));
            }
            else
            {
                logger.LogDebug("No countries found");
            }
        }
    }
}

Result information obtained

conclusion
In this article, we saw how to handle errors globally. The use of interceptors, rpcexceptions, status codes, and return information provides us with some flexibility, such as the possibility of customizing errors and sending related errors to clients. 🙂

If you want to know more about Interceptors, please leave me a message.

If you don't understand or make mistakes, please correct them
If you like, you might as well like a collection

Posted by ahundiak on Mon, 29 Jun 2020 17:54:30 -0700