Use when the user asks to "call a gRPC service", "create gRPC client", "consume gRPC service from F#", "connect to gRPC server", "write gRPC client code", "gRPC client streaming", or needs to consume gRPC services from F# client code.
Consume gRPC services from F# client code.
This skill covers creating gRPC clients in F# for both code-first (protobuf-net.Grpc) and contract-first (standard) approaches. Clients can be used in console apps, web APIs, background services, or test projects.
<!-- Client.fsproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<!-- Code-first -->
<PackageReference Include="protobuf-net.Grpc.Native" Version="1.1.1" />
<PackageReference Include="protobuf-net-fsharp" Version="0.1.0" />
<!-- OR standard contract-first -->
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.fsproj" />
</ItemGroup>
</Project>
open Grpc.Net.Client
open ProtoBuf.Grpc.Client
open ProtoBuf.FSharp
open MyApp.Shared.Contracts
// Register F# types (each record type individually)
let model = Serialiser.defaultModel
Serialiser.registerRecordRuntimeTypeIntoModel typeof<GreetRequest> model |> ignore
Serialiser.registerRecordRuntimeTypeIntoModel typeof<GreetReply> model |> ignore
// Create channel
let channel = GrpcChannel.ForAddress("http://localhost:5000")
// Create typed client from interface
let greeter = channel.CreateGrpcService<IGreeterService>()
// Call the service
let callGreeter () = task {
let request = { Name = "World" }
let! reply = greeter.SayHello(request, ProtoBuf.Grpc.CallContext.Default)
printfn $"Response: {reply.Message}"
}
open Grpc.Net.Client
open MyApp.V1 // C# generated namespace
let channel = GrpcChannel.ForAddress("http://localhost:5000")
let client = Greeter.GreeterClient(channel)
let callGreeter () = task {
let request = GreetRequest(Name = "World")
let! reply = client.SayHelloAsync(request)
printfn $"Response: {reply.Message}"
}
let subscribeToTicker () = task {
let request = { Symbol = "MSFT" }
// Code-first: returns IAsyncEnumerable
let stream = ticker.Subscribe(request, ProtoBuf.Grpc.CallContext.Default)
do! stream |> AsyncSeq.iterAsync (fun tick ->
printfn $"{tick.Symbol}: {tick.Price}"
Task.CompletedTask
)
}
// Standard approach:
let subscribeStandard () = task {
use call = client.SayHelloStream(GreetRequest(Name = "World"))
let stream = call.ResponseStream
while! stream.MoveNext(System.Threading.CancellationToken.None) do
printfn $"Got: {stream.Current.Message}"
}
let uploadData () = task {
let chunks = asyncSeq {
for i in 1..10 do
yield { Data = $"chunk-{i}"; Index = i }
}
let! result = uploader.Upload(chunks, ProtoBuf.Grpc.CallContext.Default)
printfn $"Upload result: {result.BytesReceived}"
}
let chat () = task {
let outgoing = asyncSeq {
yield { Text = "Hello"; User = "Alice" }
do! Task.Delay(1000)
yield { Text = "How are you?"; User = "Alice" }
}
let incoming = chatService.Chat(outgoing, ProtoBuf.Grpc.CallContext.Default)
do! incoming |> AsyncSeq.iterAsync (fun msg ->
printfn $"[{msg.User}]: {msg.Text}"
Task.CompletedTask
)
}
// Create once, reuse across the application lifetime
let channel = GrpcChannel.ForAddress("http://localhost:5000")
// Channels are thread-safe and multiplexed
let service1 = channel.CreateGrpcService<IGreeterService>()
let service2 = channel.CreateGrpcService<IOrderService>()
let channel =
GrpcChannel.ForAddress(
"https://api.example.com",
GrpcChannelOptions(
MaxReceiveMessageSize = 16 * 1024 * 1024, // 16 MB
MaxSendMessageSize = 16 * 1024 * 1024,
Credentials = ChannelCredentials.Insecure // Dev only
)
)
Register gRPC clients in an ASP.NET Core application:
// In ConfigureServices / builder.Services
builder.Services
.AddCodeFirstGrpcClient<IGreeterService>(fun options ->
options.Address <- System.Uri("http://localhost:5000")
)
.ConfigureChannel(fun options ->
options.MaxReceiveMessageSize <- 16 * 1024 * 1024
)
|> ignore
Then inject the service interface:
type MyController(greeter: IGreeterService) =
member _.Get() = task {
let! reply = greeter.SayHello({ Name = "Web" }, CallContext.Default)
return reply.Message
}
open Grpc.Core
let callWithErrorHandling () = task {
try
let! reply = greeter.SayHello({ Name = "" }, CallContext.Default)
printfn $"Success: {reply.Message}"
with
| :? RpcException as ex when ex.StatusCode = StatusCode.InvalidArgument ->
printfn $"Validation error: {ex.Status.Detail}"
| :? RpcException as ex when ex.StatusCode = StatusCode.Unavailable ->
printfn "Server is unavailable, retrying..."
| :? RpcException as ex ->
printfn $"gRPC error {ex.StatusCode}: {ex.Status.Detail}"
}
open System.Threading
let callWithDeadline () = task {
use cts = new CancellationTokenSource(TimeSpan.FromSeconds(5.0))
// Code-first: use CallContext with headers and cancellation
let context = CallContext(cancellationToken = cts.Token)
let! reply = greeter.SayHello({ Name = "World" }, context)
printfn $"{reply.Message}"
}
// Standard approach:
let callWithDeadlineStandard () = task {
let deadline = System.DateTime.UtcNow.AddSeconds(5.0)
let! reply = client.SayHelloAsync(
GreetRequest(Name = "World"),
deadline = deadline
)
printfn $"{reply.Message}"
}
let callWithMetadata () = task {
let headers = Metadata()
headers.Add("authorization", "Bearer my-token")
headers.Add("x-request-id", System.Guid.NewGuid().ToString())
// Code-first
let context = CallContext(requestHeaders = headers)
let! reply = greeter.SayHello({ Name = "World" }, context)
return reply
}
let retryChannel =
GrpcChannel.ForAddress(
"http://localhost:5000",
GrpcChannelOptions(
ServiceConfig = ServiceConfig(
MethodConfigs = {|
Names = [| MethodName.Default |]
RetryPolicy = RetryPolicy(
MaxAttempts = 5,
InitialBackoff = TimeSpan.FromSeconds(1.0),
MaxBackoff = TimeSpan.FromSeconds(5.0),
BackoffMultiplier = 1.5,
RetryableStatusCodes = {| StatusCode.Unavailable |}
)
|}
)
)
)
GrpcChannel instances; they are thread-safe and handle connection poolingRpcException with pattern matching on StatusCodeCancellationToken for long-running streaming calls.Result or .Wait(); use task {} computation expressionRpcException without loggingtemplates/Client.fs, templates/Client.fsprojreferences/client-patterns.mdGrpcChannel with server address