Feather.Grpc
1.1.0
dotnet add package Feather.Grpc --version 1.1.0
NuGet\Install-Package Feather.Grpc -Version 1.1.0
<PackageReference Include="Feather.Grpc" Version="1.1.0" />
<PackageVersion Include="Feather.Grpc" Version="1.1.0" />
<PackageReference Include="Feather.Grpc" />
paket add Feather.Grpc --version 1.1.0
#r "nuget: Feather.Grpc, 1.1.0"
#:package Feather.Grpc@1.1.0
#addin nuget:?package=Feather.Grpc&version=1.1.0
#tool nuget:?package=Feather.Grpc&version=1.1.0
<img src="https://github.com/FeatherTools/.github/blob/main/profile/feather-logo-200.png" alt="FeatherTools Logo" width="100" height="100"> gRPC
Library with low and high level helpers for gRPC.
Install
paket add Feather.Grpc
The conversions in this library map to the shared core proto types from
Feather.Contracts (feather.contracts.core.v1
package — Timestamp, CorrelationId, Spot, Instance, Box, Error,
SerializedForChunking, …). Reference these in your own .proto definitions so
messages interoperate across services and languages.
Copilot skill: this repo ships a
protoskill at .github/skills/proto/SKILL.md with conventions for authoring.protoservice contracts (naming, theoneof result { Success / Error }pattern, streaming) that align withFeather.Grpc.
Usage
Low- and high-level helpers for building gRPC clients and servers in F#, with streaming, chunking of large payloads, auth, error handling and metrics.
Creating a client (in k8s environment)
Build a channel to a service instance and wrap it in the generated client:
open Feather.Grpc
// Application startup
let connectCalculatorClient (environment: Map<string, string>) (myService: string) = result {
let! myServiceInstance = myService |> instance environment // Result<Instance, 'Error>
return myServiceInstance |> Grpc.k8sSvcChannel Grpc.Port |> Math.Calculator.CalculatorClient
}
let! myServiceClient = "MY_SERVICE" |> connectCalculatorClient environment
Unary calls
AsyncResult.ofAsyncUnaryResponse turns a gRPC unary call into an AsyncResult<'Response, GrpcError>,
so it composes inside an asyncResult { … } block (see Feather/Error Handling).
Convert contract types at the boundary with ofContract / asContract.
The example below is a Calculator service exposing a single Divide call.
Proto definition — reuse the shared core types (Spot, Error) from
Feather.Contracts. Add the BSR module as a
dependency in your buf.yaml:
version: v2
deps:
- buf.build/feathertools/contracts
syntax = "proto3";
package calculator;
option csharp_namespace = "Math";
import "feather/contracts/core/v1/core.proto";
message Input { int32 value = 1; }
message Output { int32 value = 1; }
message DivideRequest {
Input base = 1;
Input divider = 2;
}
message DivideResponse {
oneof result {
Success success = 1;
feather.contracts.core.v1.Error error = 2;
}
message Success {
Output output = 1;
}
}
service Calculator {
rpc Divide (DivideRequest) returns (DivideResponse);
}
Domain library — the ofContract / asContract pairs convert between the generated
proto messages and your domain types:
namespace Domain
open Feather.Grpc
type Input = Input of int
type Output = Output of int
[<RequireQualifiedAccess>]
module Input =
let ofContract (contract: Math.Input): Result<Input, ContractError> =
// it could have some validation, ...
contract.Value |> Input |> Ok
let asContract (Input value): Math.Input =
Math.Input(Value = value)
[<RequireQualifiedAccess>]
module Output =
let ofContract (contract: Math.Output): Result<Output, ContractError> =
// it could have some validation, ...
contract.Value |> Output |> Ok
let asContract (Output value): Math.Output =
Math.Output(Value = value)
Calculator Service implementation
open Feather.Grpc
open Feather.Grpc.Metrics
// your app metrics, built once at startup via `GrpcMetrics.metrics currentInstance`
type AppMetrics = {
// ... app specific metrics ...
GrpcMetrics: GrpcMetrics
}
type ApplicationDependencies = {
LoggerFactory: ILoggerFactory
Metrics: AppMetrics
}
type CalculatorImplementation(app: ApplicationDependencies) =
inherit Calculator.CalculatorBase()
override _.Divide (request: DivideRequest, context: ServerCallContext): Task<DivideResponse> = task {
let logger = app.LoggerFactory.CreateLogger("Divide")
// pure operation, returning a success response or GrpcError
let operation request = asyncResult {
let! (Input value) = request.Base |> Input.ofContract |> Result.mapError GrpcError.ofContractError
let! (Input divider) = request.Divider |> Input.ofContract |> Result.mapError GrpcError.ofContractError
logger.LogDebug("Calculating: {value} / {divider}", value, divider)
if divider = 0 then
// GrpcError is automatically bind to an Result.Error by predefined helper
return! GrpcError.create "DivisionByZero" (Some $"{value} / {divider}")
let output = Output (value / divider)
return DivideResponse.Types.Success(
Output = (output |> Output.asContract)
)
}
return!
request
|> operation
|> HighLevel.Response.handle app.Metrics.GrpcMetrics "Divide" logger None
(fun success -> DivideResponse(Success = success))
(fun error -> DivideResponse(Error = error))
}
Calling a service by its client
let example (myServiceClient: Calculator.CalculatorClient) (a: Domain.Input) (b: Domain.Input) = asyncResult {
let! (response: DivideResponse) =
myServiceClient.DivideAsync(
DivideRequest(
Base = Input.asContract a,
Divider = Input.asContract b
)
)
|> AsyncResult.ofAsyncUnaryResponse
match response.ResultCase with
| DivideResponse.ResultOneofCase.Success ->
let! (Output value) = response.Success.Output |> Output.ofContract |> Result.mapError GrpcError.ofContractError
return value
| _ ->
return! response.Error |> GrpcError.ofContract // possibly division by zero
}
Streaming large payloads
SerializedForChunking serializes a DTO once and splits it into ~256 KB chunks so it
can be streamed over gRPC (plain, gzip, raw bytes or text):
let ct = context.CancellationToken // core gRPC request context contains its cancellation, it should be used
// server side: stream a value out
value
|> HighLevel.Send.ServerStream.gzipValue ct writer serialize chunkToRequest
// client side: read the chunks back into a value
call
|> HighLevel.Read.Stream.Call.value ct handleResponse (SerializedForChunking.Dto.Gzip.fromChunks parse)
Server auth interceptor
let authenticate: AuthInterceptor<MyContext> =
fun context -> ... // Result<MyContext, AuthError>
let context = AuthInterceptor.validate authenticate serverCallContext
Release
- Increment version in
Grpc.fsproj - Update
CHANGELOG.md - Commit new version and tag it
Development
Requirements
Build
./build.sh build
Tests
./build.sh -t tests
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Alma.Authorization (>= 10.4.0 && < 11.0.0)
- Alma.ServiceIdentification (>= 11.0.0 && < 12.0.0)
- Alma.WebApplication (>= 14.0.0 && < 15.0.0)
- Feather.Contracts (>= 1.2.0 && < 2.0.0)
- Feather.Cryptography (>= 2.3.0 && < 3.0.0)
- Feather.ErrorHandling (>= 2.0.0 && < 3.0.0)
- FSharp.Control.AsyncSeq (>= 4.15.0 && < 5.0.0)
- FSharp.Core (>= 10.1.302 && < 11.0.0)
- FSharp.Data (>= 6.7.0 && < 7.0.0)
- Grpc.Net.Client (>= 2.80.0 && < 3.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.