grpc-dotnet icon indicating copy to clipboard operation
grpc-dotnet copied to clipboard

Map adhoc lambda expression to gRPC

Open JamesNK opened this issue 6 years ago • 7 comments

Today gRPC responses are always mapped to a service. What about adding some additional startup extension methods for mapping a lambda expression to a gRPC endpoint.

MapGrpcUnaryMethod<TRequest, TResponse>(
    this IEndpointRouteBuilder builder,
    string serviceName,
    string methodName,
    UnaryCallHander<TRequest, TResponse> handler);
routes.MapGrpcUnaryMethod<HelloRequest, HelloReply>("Greet.Greeter/SayHello", async (request, context) =>
{
    return new HelloReply("Hello " + request.Message);
});

One issue to solve in the example above is serialization of the request and reply. That is defined on Grpc.Core's Method type, and with code-gen a marshaller is created over the top of a protobuf IMessage for the user. Need to think of a nice looking way to give that info to MapGrpcXXXMethod.

JamesNK avatar Feb 19 '19 23:02 JamesNK

I think we should do an API review for various kinds of Map* methods we'd want to support - I've seen many ideas around what we should support in the past few days.

In general, mapping an adhoc method is something worth supporting but there are some questions around the API design:

  • why not just pass Method<,> as an argument (that includes all the info needed, including marshallers).
  • passing the lambda directly has "singleton" semantics, which is something we didn't want to use for services by default. Is there a good way to support scoped semantics here, or are we happy with an implied singleton in this case?

jtattermusch avatar Feb 20 '19 09:02 jtattermusch

  • why not just pass Method<,> as an argument (that includes all the info needed, including marshallers).

Sure we could do that. It feels like moving the problem somewhere else though. Creating a Method<,> isn't trivial.

  • passing the lambda directly has "singleton" semantics, which is something we didn't want to use for services by default. Is there a good way to support scoped semantics here, or are we happy with an implied singleton in this case?

With a full service type you can inject into its constructor using DI. That's not a concern here because we have no type, and there will never be any confusion about the lifetime of the type.

If someone wants to get something via DI in the request scope then they can retrieve it from the HttpContext:

routes.MapGrpcUnaryMethod<HelloRequest, HelloReply>("Greet.Greeter/SayHello", async (request, context) =>
{
    var dbContext = context.GetHttpContext().RequestServices.GetRequiredService<DatabaseContext>();
    // Do database things...

    return new HelloReply("Hello " + request.Message);
});

It is kind of ugly, but moving to a full service is always an option if someone wants a better DI experience.

JamesNK avatar Feb 20 '19 21:02 JamesNK

I think we should table this until a future release. We need to work on the fundamentals for now. Once we nail those we can get back to this.

davidfowl avatar Feb 22 '19 06:02 davidfowl

I think we should table this until a future release. We need to work on the fundamentals for now. Once we nail those we can get back to this.

Agreed, except this is partially about API design and it's better to not break users in the future. So it's good to be aware of other mapping use cases than the existing MapGrpcService<>()

jtattermusch avatar Feb 22 '19 08:02 jtattermusch

I don't think this is needed.

JamesNK avatar Nov 04 '19 23:11 JamesNK

So, I have an idea of how to do this.

  1. Add 4 more extension methods to IEndpointRouteBuilder
public static class GrpcEndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapGrpcUnary<TRequest, TResponse>(
        this IEndpointRouteBuilder builder,
        Method<TRequest, TResponse> method,
        UnaryServerMethod<TRequest, TResponse> invoker)
        where TRequest : class
        where TResponse : class
    {
        // Register with endpoint routing
        return null!;
    }

    // Plus...
    // MapGrpcServerStreaming
    // MapGrpcClientStreaming
    // MapGrpcDuplexStreaming
}
  1. Make Method<TRequest, TResponse> instances public in generated code
public static partial class Counter
{
    static readonly grpc::Method<global::Google.Protobuf.WellKnownTypes.Empty, global::Count.CounterReply> __Method_IncrementCount = new grpc::Method<global::Google.Protobuf.WellKnownTypes.Empty, global::Count.CounterReply>(
        grpc::MethodType.Unary,
        __ServiceName,
        "IncrementCount",
        __Marshaller_google_protobuf_Empty,
        __Marshaller_count_CounterReply);

    // This is new
    public static grpc::Method<global::Google.Protobuf.WellKnownTypes.Empty> IncrementCount => __Method_IncrementCount;
}

Now you could use adhoc lambda expressions in Startup.cs with gRPC.

app.UseEndpoints(endpoints =>
{
    // Normal service
    endpoints.MapGrpcService<NormalService>();

    // New hotness adhoc method
    var i = 0;
    endpoints.MapGrpcUnary(Counter.IncrementCount, (request, context) =>
    {
        return Task.FromResult(new CounterReply { Count = i++ });
    });
});

Not only do we get all of the routing and serialization info from Counter.IncrementCount, .NET can use the Method<TRequest, TResponse> to infer the generic arguments for MapGrpcUnary.

JamesNK avatar Dec 06 '19 22:12 JamesNK

This looks ok .

bklooste avatar Aug 24 '21 01:08 bklooste