Feather.Grpc
1.0.0
See the version list below for details.
dotnet add package Feather.Grpc --version 1.0.0
NuGet\Install-Package Feather.Grpc -Version 1.0.0
<PackageReference Include="Feather.Grpc" Version="1.0.0" />
<PackageVersion Include="Feather.Grpc" Version="1.0.0" />
<PackageReference Include="Feather.Grpc" />
paket add Feather.Grpc --version 1.0.0
#r "nuget: Feather.Grpc, 1.0.0"
#:package Feather.Grpc@1.0.0
#addin nuget:?package=Feather.Grpc&version=1.0.0
#tool nuget:?package=Feather.Grpc&version=1.0.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
grpc.contract.core
(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
grpc.contract.core:
syntax = "proto3";
package calculator;
option csharp_namespace = "Math";
import "feather/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.core.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.0.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.