FireflyFirefly
DocsGuides
GitHub

gRPC Greeter

This guide walks through a small Firefly app that serves a gRPC Greeter service with both a unary and a server-streaming method, while still exposing a plain HTTP health-check route. It shows how Firefly registers gRPC services with its grpcService computation expression and mounts them on the same host as your HTTP routes.

What you'll learn

  • Defining a gRPC service contract in a .proto file
  • Generating Protobuf message types with Grpc.Tools (messages only, no generated service stubs)
  • Implementing a gRPC service in F# with the grpcService { ... } builder
  • Registering unary and serverStream methods
  • Serving gRPC alongside ordinary HTTP routes with App.grpc

The proto contract

The service is described in protos/greeter.proto. It declares a Greeter service with two RPCs — a unary SayHello and a server-streaming SayHelloStream — plus the request and reply messages.

syntax = "proto3";
 
package greet;
 
option csharp_namespace = "Greet";
 
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}
 
message HelloRequest {
  string name = 1;
}
 
message HelloReply {
  string message = 1;
}

Generating the message types

The Protobuf messages (HelloRequest, HelloReply) are generated by Grpc.Tools from the .proto file. The example puts this in a small companion project, Protos.Generated. Note GrpcServices="None": only the message types are generated — Firefly's grpcService builder supplies the service binding, so no C# service stubs are needed.

<ItemGroup>
  <PackageReference Include="Google.Protobuf" Version="3.30.2" />
  <PackageReference Include="Grpc.Tools" Version="2.71.0" PrivateAssets="All" />
  <Protobuf Include="../protos/greeter.proto" GrpcServices="None" />
</ItemGroup>

The app project references both Firefly and this generated project:

<ItemGroup>
  <ProjectReference Include="../../src/Firefly/Firefly.fsproj" />
  <ProjectReference Include="Protos.Generated/Protos.Generated.csproj" />
</ItemGroup>

Implementing the service

The service is defined with the grpcService computation expression. You pass the fully-qualified service name ("greet.Greeter") and register each method by name. The unary method takes the decoded request and a ServerCallContext, returning a Task of the reply. The serverStream method additionally receives an IServerStreamWriter<_> that you write replies to over time.

module GrpcGreeter.App
 
open System.Threading.Tasks
open Grpc.Core
open Greet
open Firefly
 
let greeter = grpcService "greet.Greeter" {
    unary "SayHello" (fun (req: HelloRequest) (_ctx: ServerCallContext) -> task {
        return HelloReply(Message = $"Hello, {req.Name}!")
    })
    serverStream "SayHelloStream" (fun (req: HelloRequest) (writer: IServerStreamWriter<HelloReply>) (_ctx: ServerCallContext) -> task {
        for i in 1..5 do
            do! writer.WriteAsync(HelloReply(Message = $"Hello #{i}, {req.Name}!"))
            do! Task.Delay(200)
    })
}

The message types (HelloRequest, HelloReply) come from the generated Greet namespace that the .proto's csharp_namespace option specifies.

Wiring gRPC into the app

gRPC services don't replace your HTTP routes — they sit alongside them. Here a single /health HTTP route is defined the usual way, and the gRPC service is added to the app config with App.grpc.

let routes =
    Route.start
    |> Route.get "/health" (fun _ -> task { return Response.text "ok" })
 
let create () =
    let config =
        App.defaults
        |> App.port 5000
        |> App.grpc greeter
    (routes, config)

App.defaults provides a baseline config, App.port sets the listening port, and App.grpc registers the service. You can chain multiple App.grpc calls to host more than one service.

Startup

Program.fs builds the routes and config, then runs the app. App.run returns a task, which the example simply waits on to keep the process alive.

open System.Threading
open Firefly
open GrpcGreeter.App
 
let (routes, config) = create ()
App.run routes config CancellationToken.None |> fun t -> t.Wait()

Running it

dotnet run --project examples/grpc-greeter

The app listens on port 5000. The HTTP health check is reachable directly:

curl http://localhost:5000/health
# ok

Call the gRPC methods with grpcurl. The unary SayHello:

grpcurl -plaintext -d '{"name": "Ada"}' \
  localhost:5000 greet.Greeter/SayHello
# { "message": "Hello, Ada!" }

And the server-streaming SayHelloStream, which emits five replies ~200ms apart:

grpcurl -plaintext -d '{"name": "Ada"}' \
  localhost:5000 greet.Greeter/SayHelloStream

Source

The full example lives in examples/grpc-greeter/ — see App.fs, Program.fs, protos/greeter.proto, and the Protos.Generated project.