| name | fsgrpc-proto |
| description | Use when the user asks to "write a proto file", "create protobuf definitions", "generate F# from proto", "set up proto compilation", "define gRPC service contract", "convert proto to F#", or needs to work with .proto files and generate F# code from them. |
| version | 0.1.0 |
Contract-First gRPC with .proto Files
Write .proto files and generate idiomatic F# code from them.
Overview
Contract-first gRPC starts with .proto files that define messages and
services, then generates code. For F#, there are two generation paths:
| Generator | Output | F# Idiom Level | Requirements |
|---|
| FsGrpc | Immutable F# records, DUs for oneof | High | buf CLI, FsGrpc NuGet |
| Grpc.Tools | C# mutable classes | Low (interop) | Grpc.Tools NuGet |
Recommendation: Use FsGrpc for new F# projects. Use Grpc.Tools only when
you must share generated code with C# projects.
Workflow
1. Write the .proto file
Create proto files in a Protos/ directory. Use proto3 syntax:
syntax = "proto3";
package myapp.v1;
option csharp_namespace = "MyApp.V1";
// Request message
message GreetRequest {
string name = 1;
}
// Response message
message GreetReply {
string message = 1;
}
// Service definition
service Greeter {
// Unary RPC
rpc SayHello (GreetRequest) returns (GreetReply);
// Server streaming RPC
rpc SayHelloStream (GreetRequest) returns (stream GreetReply);
// Client streaming RPC
rpc CollectNames (stream GreetRequest) returns (GreetReply);
// Bidirectional streaming RPC
rpc Chat (stream GreetRequest) returns (stream GreetReply);
}
2. Proto file conventions
| Convention | Rule |
|---|
| File names | lower_snake_case.proto |
| Package | company.project.version (e.g., myapp.v1) |
| Message names | PascalCase |
| Field names | lower_snake_case |
| Enum values | UPPER_SNAKE_CASE with type prefix |
| Service names | PascalCase |
| RPC names | PascalCase |
3. Common proto3 patterns
Enums
enum Status {
STATUS_UNSPECIFIED = 0; // Always have a zero value
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
Nested messages
message Order {
string id = 1;
message LineItem {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
repeated LineItem items = 2;
}
Oneof (maps to F# DU with FsGrpc)
message PaymentMethod {
oneof method {
CreditCard credit_card = 1;
BankTransfer bank_transfer = 2;
string voucher_code = 3;
}
}
FsGrpc generates:
type PaymentMethod =
{ Method: PaymentMethodCase }
and PaymentMethodCase =
| CreditCard of CreditCard
| BankTransfer of BankTransfer
| VoucherCode of string
Well-known types
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/duration.proto";
message Event {
google.protobuf.Timestamp created_at = 1;
google.protobuf.StringValue optional_note = 2; // nullable
google.protobuf.Duration timeout = 3;
}
Maps
message Config {
map<string, string> settings = 1;
}
FsGrpc generates Map<string, string> in F#.
4. Generate F# code with FsGrpc
Set up buf.yaml
version: v1
deps:
- buf.build/googleapis/googleapis
breaking:
use:
- FILE
lint:
use:
- DEFAULT
Run generation
Generation is an explicit buf generate step. Earlier versions of this skill
claimed FsGrpc.Tools hooks into MSBuild and regenerates on dotnet build;
that is incorrect for currently-available tooling — FsGrpc.Tools 1.0.6 is
not published on nuget.org and the published 0.6.3 still requires the
protoc-gen-fsgrpc plugin binary on PATH, so MSBuild cannot bootstrap
generation on a clean machine on its own.
Prerequisites:
buf on PATH (see fsgrpc-setup skill for install commands).
protoc-gen-fsgrpc on PATH. Install with
fsgrpc-setup/scripts/install-protoc-gen-fsgrpc.sh — that script builds
the plugin from a pinned dmgtech/fsgrpc commit, patches it to match
FsGrpc 1.0.6 runtime, and drops a wrapper into $HOME/.local/bin/.
With buf.gen.yaml at the root of your Protos/ workspace:
version: v1
plugins:
- plugin: fsgrpc
out: ../Generated
opt:
- paths=source_relative
Then:
cd Protos/
buf generate
Commit the generated .fs files to source control so downstream consumers
can dotnet build without needing the plugin installed. Re-run
buf generate whenever a .proto file changes.
5. Generated F# code patterns (FsGrpc)
FsGrpc generates:
| Proto concept | F# output |
|---|
message | Immutable F# record |
enum | F# enum type |
oneof | F# discriminated union |
repeated | list<T> |
map<K,V> | Map<K,V> |
optional scalar | Value with default |
google.protobuf.StringValue | string option |
google.protobuf.Timestamp | System.DateTimeOffset |
6. Using standard Grpc.Tools (C# interop path)
If you must use the standard toolchain:
- Create a C# class library for the generated code:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" />
<Protobuf Include="..\Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
</Project>
- Reference from F# project:
<ProjectReference Include="..\ProtoLib\ProtoLib.csproj" />
- Consume in F#:
open MyApp.V1
let request = GreetRequest(Name = "World")
// Note: these are mutable C# objects, not idiomatic F#
Best Practices
- DO use proto3 syntax for all new definitions
- DO always include a zero-value
UNSPECIFIED entry in enums
- DO use
oneof for variant types; FsGrpc maps them to DUs
- DO version your packages (e.g.,
myapp.v1, myapp.v2)
- DO use well-known types (
Timestamp, wrappers) instead of raw primitives for nullable/temporal fields
- DO run
buf lint to validate proto files
- DON'T change field numbers in published protos (breaks wire compatibility)
- DON'T remove or rename fields; use
reserved instead
- DON'T use
required (not available in proto3 anyway)
- DON'T put C# generated code directly in an F# project; use a C# intermediary
Additional Resources
Implementation Workflow
- Create
Protos/ directory and buf.yaml
- Write
.proto file(s) following conventions
- Run
buf lint to validate
- Build project to trigger code generation
- Verify generated F# code compiles and types are correct
- Use generated types in server/client code