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
.protofile - Generating Protobuf message types with
Grpc.Tools(messages only, no generated service stubs) - Implementing a gRPC service in F# with the
grpcService { ... }builder - Registering
unaryandserverStreammethods - 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-greeterThe app listens on port 5000. The HTTP health check is reachable directly:
curl http://localhost:5000/health
# okCall 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/SayHelloStreamSource
The full example lives in examples/grpc-greeter/ — see App.fs,
Program.fs, protos/greeter.proto, and the Protos.Generated project.
