grpc / grpc-dotnet

gRPC for .NET
Apache License 2.0
4.17k stars 769 forks source link

Map adhoc lambda expression to gRPC #111

Open JamesNK opened 5 years ago

JamesNK commented 5 years ago

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.

jtattermusch commented 5 years ago

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:

JamesNK commented 5 years ago
  • 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.

davidfowl commented 5 years ago

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.

jtattermusch commented 5 years ago

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<>()

JamesNK commented 4 years ago

I don't think this is needed.

JamesNK commented 4 years ago

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.

bklooste commented 3 years ago

This looks ok .