ServiceModel.Grpc

Code-first for gRPC

View on GitHub

ServiceModel.Grpc global error handling in gRPC

ServiceModel.Grpc does not handle any errors by default. The general information about error handling is here.

This tutorial shows how to implement custom gRPC exception handling with the following behavior:

View sample code.

Create a contract

The service contract:

[ServiceContract]
public interface IDebugService
{
    // always throws ApplicationException with a specific message
    [OperationContract]
    Task ThrowApplicationException(string message);

    // randomly throws InvalidOperationException or NotSupportedException with a specific message
    [OperationContract]
    Task ThrowRandomException(string message);
}

UnexpectedErrorDetail is a data contract and caries detailed information about InvalidOperationException or NotSupportedException from server to client.

[DataContract]
public class UnexpectedErrorDetail
{
    [DataMember]
    public string Message { get; set; }

    [DataMember]
    public string MethodName { get; set; }

    [DataMember]
    public string ExceptionType { get; set; }

    [DataMember]
    public string FullException { get; set; }
}

Raise errors in gRPC server

The service implementation is simple

public sealed class DebugService : IDebugService
{
    public Task ThrowApplicationException(string message)
    {
        throw new ApplicationException(message);
    }

    public Task ThrowRandomException(string message)
    {
        var randomValue = new Random(DateTime.Now.Millisecond).Next(0, 2);
        if (randomValue == 0)
        {
            throw new InvalidOperationException(message);
        }

        throw new NotSupportedException(message);
    }
}

Catch errors in gRPC clients

// IClientFactory DefaultClientFactory
var client = DefaultClientFactory.CreateClient<IDebugService>(...);

// catch ApplicationException
try
{
    await client.ThrowApplicationException("  application error occur");
}
catch (ApplicationException ex)
{
    Console.WriteLine(ex.Message);
}

// catch UnexpectedErrorException which is defined on client
try
{
    await client.ThrowRandomException("random error occur");
}
catch (UnexpectedErrorException ex)
{
    Console.WriteLine("  Message: {0}", ex.Detail.Message);
    Console.WriteLine("  ExceptionType: {0}", ex.Detail.ExceptionType);
    Console.WriteLine("  MethodName: {0}", ex.Detail.MethodName);
    Console.WriteLine("  FullException: {0} ...", new StringReader(ex.Detail.FullException).ReadLine());
}

Create server error handlers

A server-side error handler is an implementation of interface IServerErrorHandler with one method ProvideFaultOrIgnore. If the method returns null, the exception is processed by gRPC API, otherwise ServiceModel.Grpc throws an RpcException based on ServerFaultDetail.

To meet the requirements we implement two error handlers. The first one ApplicationExceptionServerHandler is responsible to process ApplicationException.

public sealed class ApplicationExceptionServerHandler : IServerErrorHandler
{
    public ServerFaultDetail? ProvideFaultOrIgnore(ServerCallInterceptorContext context, Exception error)
    {
        if (error is ApplicationException)
        {
            // provide a marker for the client exception handler
            return new ServerFaultDetail { Detail = "ApplicationException" };
        }

        // ignore other exceptions
        return null;
    }
}

The second one UnexpectedExceptionServerHandler is responsible to process InvalidOperationException and NotSupportedException.

public sealed class UnexpectedExceptionServerHandler : IServerErrorHandler
{
    public ServerFaultDetail? ProvideFaultOrIgnore(ServerCallInterceptorContext context, Exception error)
    {
        if (error is NotSupportedException || error is InvalidOperationException)
        {
            // provide detailed information for the client error handler
            var detail = new UnexpectedErrorDetail
            {
                Message = error.Message,
                ExceptionType = error.GetType().FullName,
                FullException = error.ToString(),
                MethodName = context.ServerCallContext.Method
            };

            return new ServerFaultDetail { Detail = detail };
        }

        // ignore other exceptions
        return null;
    }
}

All other exceptions are handled by gRPC.

Create client error handlers

A client-side error handler is an implementation of interface IClientErrorHandler with one method ThrowOrIgnore. The method can ignore original RpcException, provided by gRPC API, or throws a custom exception to pass it to the caller.

To meet the requirements we implement two error handlers. The first one ApplicationExceptionClientHandler is responsible to process marker ApplicationException provided by the server error handler.

internal sealed class ApplicationExceptionClientHandler : IClientErrorHandler
{
    public void ThrowOrIgnore(ClientCallInterceptorContext context, ClientFaultDetail detail)
    {
        // if marker is ApplicationException
        if ((detail.Detail is string name) && name == "ApplicationException")
        {
            // throw custom exception
            throw new ApplicationException(detail.OriginalError.Status.Detail);
        }
    }
}

The second one UnexpectedExceptionClientHandler is responsible to process marker UnexpectedErrorDetail provided by server error handler.

// custom exception with detailed information from server
public class UnexpectedErrorException : SystemException
{
    public UnexpectedErrorException(UnexpectedErrorDetail detail)
        : base(detail.Message)
    {
        Detail = detail;
    }

    public UnexpectedErrorDetail Detail { get; }
}

internal sealed class UnexpectedExceptionClientHandler : IClientErrorHandler
{
    public void ThrowOrIgnore(ClientCallInterceptorContext context, ClientFaultDetail detail)
    {
        // if marker is UnexpectedErrorDetail
        if (detail.Detail is UnexpectedErrorDetail unexpectedErrorDetail)
        {
            // throw custom exception
            throw new UnexpectedErrorException(unexpectedErrorDetail);
        }
    }
}

All other exceptions are handled by gRPC.

Configure global error handling in asp.net core server

An error handler can be attached globally, for all ServiceModel.Grpc services.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddServiceModelGrpc(options =>
        {
            options.DefaultErrorHandlerFactory = serviceProvider => serviceProvider.GetRequiredService<IServerErrorHandler>();
        });
}

Or can be attached for a specific service.

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddServiceModelGrpcServiceOptions<DebugService>(options =>
        {
            options.ErrorHandlerFactory = serviceProvider => serviceProvider.GetRequiredService<IServerErrorHandler>();
        });
}

In case there is a global error handler and a handler for a specific service. The global one is ignored.

In this example we register a global error handler.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IServerErrorHandler>(_ =>
    {
        // combine application and unexpected handlers into one handler
        var collection = new ServerErrorHandlerCollection(
            new ApplicationExceptionServerHandler(),
            new UnexpectedExceptionServerHandler());

        return collection;
    });

    services
        .AddServiceModelGrpc(options =>
        {
            options.DefaultErrorHandlerFactory = serviceProvider => serviceProvider.GetRequiredService<IServerErrorHandler>();
        });
}

Configure global error handling in Grpc.Core.Server

An error handler can be attached for a specific service.

Grpc.Core.Server server = ...;

server.Services.AddServiceModelSingleton(
    new DebugService(),
    options =>
    {
        // combine application and unexpected handlers into one handler
        options.ErrorHandler = new ServerErrorHandlerCollection(
            new ApplicationExceptionServerHandler(),
            new UnexpectedExceptionServerHandler());
    });

Configure global error handling in client

An error handler can be attached globally, for all ServiceModel.Grpc service proxies.

IClientFactory factory = new ClientFactory(new ServiceModelGrpcClientOptions
{
    ErrorHandler = ...
});

Or can be attached for a specific service.

IClientFactory factory = new ClientFactory();
factory.AddClient<IDebugService>(options =>
{
    options.ErrorHandler = ...
});

In case there is a global error handler and a handler for a specific proxy. The global one is ignored.

In this example we register a global error handler.

private static readonly IClientFactory DefaultClientFactory = new ClientFactory(new ServiceModelGrpcClientOptions
{
    // combine application and unexpected handlers into one handler
    ErrorHandler = new ClientErrorHandlerCollection(new ApplicationExceptionClientHandler(), new UnexpectedExceptionClientHandler())
});

Run the application

View sample code.