ServiceModel.Grpc

Code-first for gRPC

View on GitHub

ServiceModel.Grpc client filters

The client filter is a hook for contract method invocation, it can work together with gRPC client interceptors, but it is not an interceptor.

see example

public sealed class MyClientFilter : IClientFilter
{
    public void Invoke(IClientFilterContext context, Action next)
    {
        // take control before all others filters in the stack and the blocking service call
        try
        {
            // invoke all other filters in the stack and call the service
            next();

            // take control after all others filters in the stack and the service call
        }
        catch
        {
            // handle the exception
            throw;
        }
        finally
        {
        }
    }

    public ValueTask InvokeAsync(IClientFilterContext context, Func<ValueTask> next)
    {
        // take control before all others filters in the stack and the async service call
        try
        {
            // invoke all other filters in the stack and call the service
            await next().ConfigureAwait(false);

            // take control after all others filters in the stack and the service call
        }
        catch
        {
            // handle the exception
            throw;
        }
        finally
        {
        }
    }
}

The client filters can work together with gRPC client interceptors.

For example, at runtime there are gRPC native interceptor1, gRPC native interceptor2, client filter1 and client filter2. Together with gRPC client interceptors the execution stack for service call looks like this:

client filter1 takes control and calls next
    client filter2 takes control and calls next
        gRPC interceptor1 takes control and calls next
            gRPC interceptor2 takes control and calls next
                service call
            gRPC interceptor2 takes control after
        gRPC interceptor1 takes control after
    client filter2 takes control after
client filter1 takes control after

Client filters registration

The registration has

A filter can be attached

var services = new ServiceCollection();

// decide the filter lifetime
services.AddSingleton<MySingletonFilter>();

IClientFactory clientFactory = new ClientFactory(new ServiceModelGrpcClientOptions
{
    ServiceProvider = services.BuildServiceProvider(),
    Filters =
    {
        // attach the filter globally to all calls from all clients
        { 1, provider => provider.GetRequiredService<MySingletonFilter>() }
    }
});

clientFactory.AddClient<IMyContract>(options =>
{
    // attach the filter to all calls from IMyContract client
    options.Filters.Add(2, _ => new MyTransientFilter())
})

Client filters context

The context is represented by interface IClientFilterContext:

void IClientFilter.Invoke(IClientFilterContext context, Action next);

ValueTask IClientFilter.InvokeAsync(IClientFilterContext context, Func<ValueTask> next);

public interface IClientFilterContext
{
    // Gets the gRPC method metadata.
    IMethod Method { get; }

    // Gets gRPC CallOptions
    CallOptions CallOptions { get; }

    // Gets your service provider.
    IServiceProvider ServiceProvider { get; }

    // Gets a dictionary that can be used by the various filters of this call to store arbitrary state.
    IDictionary<object, object?> UserState { get; }

    // Gets the the contract method declaration.
    MethodInfo ContractMethodInfo { get; }

    // Gets the control of the outgoing request.
    IRequestContext Request { get; }

    // Gets the control of the incoming response.
    IResponseContext Response { get; }
}

Unary call context example

For more details see unary operation

[ServiceContract]
interface IMyService
{
    [OperationContract]
    Task<string> Call(int arg1, int arg2, int arg3, CancellationToken token);
}

Client streaming call context example

For more details see client streaming operation

[ServiceContract]
interface IMyService
{
    [OperationContract]
    Task<string> Call(IAsyncEnumerable<int> stream, int arg1, int arg2, CancellationToken token);
}

Server streaming call context example

For more details see server streaming operation

[ServiceContract]
interface IMyService
{
    [OperationContract]
    ValueTask<(IAsyncEnumerable<string> Stream, string Metadata1, int Metadata2)> Call(int arg1, int arg2, CancellationToken token);
}

Duplex streaming call context example

For more details see duplex streaming operation

[ServiceContract]
interface IMyService
{
    [OperationContract]
    ValueTask<(IAsyncEnumerable<string> Stream, string Metadata1, int Metadata2)> Call(IAsyncEnumerable<int> stream, int arg1, int arg2, CancellationToken token);
}

The filter has complete control over the service call

In the following example, the filter replaces the service call SumAsync(1, 2) => 3.

[ServiceContract]
interface IMyService
{
    [OperationContract]
    ValueTask<int> SumAsync(int x, int y);
}

internal sealed class SumAsyncClientFilter : IClientFilter
{
    // SumAsync is async unary call
    public void Invoke(IClientFilterContext context, Action next) => next();

    public ValueTask InvokeAsync(IClientFilterContext context, Func<ValueTask> next)
    {
        // ignore all calls, except SumAsync
        if (context.ContractMethodInfo.Name != nameof(IMyService.SumAsync))
        {
            return next();
        }

        var x = (int)context.Request["x"];
        var y = (int)context.Request["y"];

        // do not invoke the real call to SumAsync
        // await next().ConfigureAwait(false);

        context.Response["result"] = x + y;
        
        return ValueTask.CompletedTask;
    }
}

Blocking call and Async call

Depending on the contract method definition, ServiceModel.Grpc infrastructure invokes IClientFilter.Invoke or IClientFilter.InvokeAsync. In general, if a method`s return type is Task or ValueTask then InvokeAsync otherwise Invoke.

[ServiceContract]
interface IMyService
{
    // IClientFilter.Invoke
    [OperationContract]
    [void|TResult] BlockingCall();

    // IClientFilter.InvokeAsync
    [OperationContract]
    [Task|ValueTask|Task<TResult>|ValueTask<TResult>] AsyncCall();

    // IClientFilter.InvokeAsync
    [OperationContract]
    [Task|ValueTask|Task<TResult>|ValueTask<TResult>] ClientStreamingCall(IAsyncEnumerable<TData> stream);

    // IClientFilter.Invoke
    [OperationContract]
    IAsyncEnumerable<TData> ServerStreamingCall();

    // IClientFilter.InvokeAsync
    [OperationContract]
    [Task<IAsyncEnumerable<TData>>|ValueTask<IAsyncEnumerable<TData>>] ServerStreamingCall();
    
    // IClientFilter.Invoke
    [OperationContract]
    IAsyncEnumerable<TData> DuplexStreamingCall(IAsyncEnumerable<TData> stream);

    // IClientFilter.InvokeAsync
    [OperationContract]
    [Task<IAsyncEnumerable<TData>>|ValueTask<IAsyncEnumerable<TData>>] ServerStreamingCall(IAsyncEnumerable<TData> stream);
}