ServiceModel.Grpc

Code-first for gRPC

View on GitHub

ServiceModel.Grpc server filters

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

see example

public sealed class MyServerFilter : IServerFilter
{
    public ValueTask InvokeAsync(IServerFilterContext context, Func<ValueTask> next)
    {
        // take control before all others filters in the stack and the service method
        try
        {
            // invoke all other filters in the stack and the service method
            await next().ConfigureAwait(false);

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

The server filters can work together with gRPC server interceptors.

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

gRPC interceptor1 takes control and calls next
    gRPC interceptor2 takes control and calls next
        server filter1 takes control and calls next
            server filter2 takes control and calls next
                service method
            server filter2 takes control after
        server filter1 takes control after
    gRPC interceptor2 takes control after
gRPC interceptor1 takes control after

Server filters registration

The registration has

A filter can be attached

in asp.net core server (ServiceModel.Grpc.AspNetCore)

// Program.cs

var builder = WebApplication.CreateBuilder();

// decide the filter lifetime
builder.Services.AddTransient<LoggingServerFilter>();

// attach the filter globally to all methods from all services
builder.Services.AddServiceModelGrpc(options =>
{
    options.Filters.Add(1, provider => provider.GetRequiredService<MySingletonFilter>());
    
    options.Filters.Add(2, _ => new MyTransientFilter());
});

// or attach the filter to all methods from MyService
builder.Services.AddServiceModelGrpcServiceOptions<MyService>(options =>
{
    options.Filters.Add(1, provider => provider.GetRequiredService<MySingletonFilter>());

    options.Filters.Add(2, _ => new MyTransientFilter());
});

in Grpc.Core.Server (ServiceModel.Grpc.SelfHost)

Grpc.Core.Server server = ...;

services.AddTransient<MyService> = ...;
services.AddSingleton<MySingletonFilter>();

IServiceProvider serviceProvider = ...;

// attach the filter to all methods from MyService
server.Services.AddServiceModel<MyService>(
    serviceProvider,
    options =>
    {
        options.Filters.Add(1, provider => provider.GetRequiredService<MySingletonFilter>());

        options.Filters.Add(2, _ => new MyTransientFilter());
    });

in the source code via ServerFilterAttribute

public sealed class MyServerFilterAttribute : ServerFilterAttribute
{
    public MyServerFilterAttribute(int order)
        : base(order)
    {
    }

    public override ValueTask InvokeAsync(IServerFilterContext context, Func<ValueTask> next)
    {
        // filter logic here
        // ...
        return next();
    }
}

// attach the filter to all methods
[MyServerFilter(1)]
class MyService : IMyService
{
    // attach the filter to the method
    [MyServerFilter(1)]
    public Task MyMethod(...) { }
}

in the source code via ServerFilterRegistrationAttribute

public sealed class MyServerFilterRegistrationAttribute : ServerFilterRegistrationAttribute
{
    public MyServerFilterRegistration(int order)
        : base(order)
    {
    }

    public override IServerFilter CreateFilter(IServiceProvider serviceProvider)
    {
        return new MyServerFilter();

        // or
        return serviceProvider.GetRequiredService<MyServerFilter>();
    }
}

// attach the filter to all methods
[MyServerFilterRegistration(1)]
class MyService : IMyService
{
    // attach the filter to the method
    [MyServerFilterRegistration(1)]
    public Task MyMethod(...) { }
}

Server filters context

The context is represented by interface IServerFilterContext:

ValueTask IServerFilter.InvokeAsync(IServerFilterContext context, Func<ValueTask> next)

public interface IServerFilterContext
{
    // Gets the service instance.
    object ServiceInstance { get; }

    // Gets gRPC ServerCallContext
    ServerCallContext ServerCallContext { get; }

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

    // Gets a dictionary that can be used by the various interceptors and handlers of this call to store arbitrary state.
    // The reference to ServerCallContext.UserState.
    IDictionary<object, object?> UserState { get; } // return ServerCallContext.UserState

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

    // Gets the the service method declaration.
    MethodInfo ServiceMethodInfo { get; }

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

    // Gets the control of the outgoing 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);
}

class MyService
{
    Task<string> Call(int renamedArg1, int renamedArg2, int renamedArg3, 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);
}

class MyService
{
    Task<string> Call(IAsyncEnumerable<int> stream, int renamedArg1, int renamedArg2, 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);
}

class MyService
{
    ValueTask<(IAsyncEnumerable<string>, string, int)> Call(int renamedArg1, int renamedArg2, 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);
}

class MyService
{
    ValueTask<(IAsyncEnumerable<string>, string, int)> Call(IAsyncEnumerable<int> stream, int renamedArg1, int renamedArg2, CancellationToken token) { ... }
}

The filter has complete control over the method call

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

[SumAsyncServerFilter]
public ValueTask<int> SumAsync(int x, int y)
{
    // the filter must handle the call
    throw new NotImplementedException();
}

internal sealed class SumAsyncServerFilterAttribute : ServerFilterAttribute
{
    public override ValueTask InvokeAsync(IServerFilterContext context, Func<ValueTask> next)
    {
        var x = (int)context.Request["x"];
        var y = (int)context.Request["y"];

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

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

In the following “HappyDebugging” example, the filter hacks the service method. Client call: MultiplyBy(values: { 1, 2 }, multiplier: 3).
Response for client: values: { 11, 16 }, multiplier: 5

sealed class Calculator
{
    [HappyDebuggingServerFilter]
    public ValueTask<(IAsyncEnumerable<int> Values, int Multiplier)> MultiplyBy(IAsyncEnumerable<int> values, int multiplier, CancellationToken token)
    {
        var output = DoMultiplyBy(values, multiplier, token);
        return new ValueTask<(IAsyncEnumerable<int>, int)>((output, multiplier));
    }

    private async IAsyncEnumerable<int> DoMultiplyBy(IAsyncEnumerable<int> values, int multiplier, [EnumeratorCancellation] CancellationToken token)
    {
        await foreach (var value in values.WithCancellation(token).ConfigureAwait(false))
        {
            yield return value * multiplier;
        }
    }
}

sealed class HappyDebuggingServerFilterAttribute : ServerFilterAttribute
{
    public override async ValueTask InvokeAsync(IServerFilterContext context, Func<ValueTask> next)
    {
        var inputMultiplier = (int)context.Request["multiplier"];
        var inputValues = (IAsyncEnumerable<int>)context.Request.Stream;

        // increase multiplier by 2
        context.Request["multiplier"] = inputMultiplier + 2;

        // increase each input value by 1
        context.Request.Stream = IncreaseValuesBy1(inputValues, context.ServerCallContext.CancellationToken);

        // call Calculator.MultiplyBy
        await next().ConfigureAwait(false);

        var outputValues = (IAsyncEnumerable<int>)context.Response.Stream;

        // increase output value by 1
        context.Response.Stream = IncreaseValuesBy1(outputValues, context.ServerCallContext.CancellationToken);
    }

    private async IAsyncEnumerable<int> IncreaseValuesBy1(IAsyncEnumerable<int> values, [EnumeratorCancellation] CancellationToken token)
    {
        await foreach (var value in values.WithCancellation(token).ConfigureAwait(false))
        {
            yield return value + 1;
        }
    }
}