ServiceModel.Grpc

Code-first for gRPC

View on GitHub

Migrate from WCF FaultContract to a gRPC global error handling with ServiceModel.Grpc

This tutorial proposes a solution how to migrate an existing WCF FaultContract exception handling to gRPC error handling with minimum effort.

How to migrate an existing WCF service and client is described here.

How to activate and configure custom error handling in gRPC is described here.

View sample code.

The existing contract and service implementation.

[ServiceContract]
public interface IDebugService
{
    // always throws FaultException<ApplicationExceptionFaultDetail>
    [OperationContract]
    [FaultContract(typeof(ApplicationExceptionFaultDetail))]
    Task ThrowApplicationException(string message);

    // always throws FaultException<InvalidOperationExceptionFaultDetail>
    [OperationContract]
    [FaultContract(typeof(InvalidOperationExceptionFaultDetail))]
    Task ThrowInvalidOperationException(string message);
}

The existing client calls

IDebugService proxy = ...;

private static async Task CallThrowApplicationException(IDebugService proxy)
{
    Console.WriteLine("WCF call ThrowApplicationException");

    try
    {
        await proxy.ThrowApplicationException("some message");
    }
    catch (FaultException<ApplicationExceptionFaultDetail> ex)
    {
        Console.WriteLine("  Error message: {0}", ex.Detail.Message);
    }
}

private static async Task CallThrowInvalidOperationException(IDebugService proxy)
{
    Console.WriteLine("WCF call ThrowInvalidOperationException");

    try
    {
        await proxy.ThrowInvalidOperationException("some message");
    }
    catch (FaultException<InvalidOperationExceptionFaultDetail> ex)
    {
        Console.WriteLine("  Error message: {0}", ex.Detail.Message);
        Console.WriteLine("  StackTrace: {0}", ex.Detail.StackTrace);
    }
}

The goal is to migrate to gRPC without any changes in existing service and client implementations.

Create server error handler

In order to provide fault detail from server to client via gRPC, an server error handler is required. The handler processes only 2 specific exceptions and ignores others:

internal sealed class FaultExceptionServerHandler : ServerErrorHandlerBase
{
    protected override ServerFaultDetail? ProvideFaultOrIgnoreCore(ServerCallInterceptorContext context, Exception error)
    {
        // handle FaultException<ApplicationExceptionFaultDetail>
        if (error is FaultException<ApplicationExceptionFaultDetail> appFault)
        {
            // pass detail to a client call
            return new ServerFaultDetail
            {
                Detail = appFault.Detail
            };
        }

        // handle FaultException<InvalidOperationExceptionFaultDetail>
        if (error is FaultException<InvalidOperationExceptionFaultDetail> opFault)
        {
            // pass detail to a client call
            return new ServerFaultDetail
            {
                Detail = opFault.Detail
            };
        }

        // ignore other error
        return null;
    }
}

Create client error handler

In order to pass FaultException<> to existing client implementation, an client error handler is required. The handler processes only 2 specific details, provided by server error handler an ignores other exceptions:

internal sealed class FaultExceptionClientHandler : ClientErrorHandlerBase
{
    protected override void ThrowOrIgnoreCore(ClientCallInterceptorContext context, ClientFaultDetail detail)
    {
        // handle ApplicationExceptionFaultDetail
        if (detail.Detail is ApplicationExceptionFaultDetail appDetail)
        {
            throw new FaultException<ApplicationExceptionFaultDetail>(appDetail);
        }

        // handle InvalidOperationExceptionFaultDetail
        if (detail.Detail is InvalidOperationExceptionFaultDetail opDetail)
        {
            throw new FaultException<InvalidOperationExceptionFaultDetail>(opDetail);
        }

        // ignore other errors
    }
}

Configure global error handling in asp.net core server

Register a global error handler.

// DebugModule.cs
container.RegisterType<IServerErrorHandler, FaultExceptionServerHandler>(new ContainerControlledLifetimeManager());

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // enable ServiceModel.Grpc
    services.AddServiceModelGrpc(options =>
    {
        // register server error handler
        options.DefaultErrorHandlerFactory = serviceProvider => serviceProvider.GetRequiredService<IServerErrorHandler>();
    });
}

Configure global error handling in Grpc.Core.Server

Attach the error handler to DebugService.

Server server = ...;

server.Services.AddServiceModelTransient(
    container.Resolve<Func<DebugService>>(),
    options =>
    {
        // register server error handler
        options.ErrorHandler = container.Resolve<IServerErrorHandler>();
    });

Configure global error handling in client

Register a global error handler.

private static readonly IClientFactory DefaultClientFactory = new ClientFactory(new ServiceModelGrpcClientOptions
{
    // register client error handler
    ErrorHandler = new FaultExceptionClientHandler()
});

Run the application

View sample code.

Clean-up

After the migration is done. FaultContract attributes can be removed from the service contract.

[ServiceContract]
public interface IDebugService
{
    [OperationContract]
    // [FaultContract(typeof(ApplicationExceptionFaultDetail))]
    Task ThrowApplicationException(string message);

    [OperationContract]
    // [FaultContract(typeof(InvalidOperationExceptionFaultDetail))]
    Task ThrowInvalidOperationException(string message);
}