protobuf-net / protobuf-net.Grpc

GRPC bindings for protobuf-net and grpc-dotnet
Other
857 stars 109 forks source link

Support for F# Discriminated Unions in data contracts #81

Open li-vu opened 4 years ago

li-vu commented 4 years ago

Following up on the discussion on Stackoverflow https://stackoverflow.com/questions/61082096/grpc-why-does-callinvoker-need-to-impose-trequest-class-and-tresponse-c/61082178.

F# discriminated unions (DUs) cannot be marshalled without manual registration of the type structure in RuntimeTypeModel. Given that DUs are very commonly used in F#, a native support would be greatly appreciated.

A use-case is to avoid throwing exceptions when something went wrong, instead returning a DU that can either be a success with the result type, or a failure containing the error.

type Response<'value, 'error> =
    | Success of 'value
    | Failure of 'error

In order to workaround this limitation, I have to manually register the type to RuntimeTypeModel using Serialiser.registerRuntimeTypeIntoModel in the package https://github.com/mvkara/protobuf-net-fsharp. While this works, it requires the extra registration for every DU type.

I wonder if DUs can either be supported natively without manual registration, or RuntimeTypeModel can be opened for extension so that users can easily provide a custom type model for a group of types, i.e. custom type model provider, instead of only being able to add a concrete type as of now.

Details on DUs

https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions#struct-discriminated-unions

Some examples of DUs and how they are handled under-the-hood.

  1. DUs with at least one union case constructor with paramters.
type Response<'value, 'error> =
    | Success of 'value
    | Failure of 'error

Under the hood, this is translated to an abstract class Response, and two sub classes Success and Failure that inherit Response.

  1. Enum-like DUs
type Status = 
    | Success
    | Failed

This is translated to a class Status with a tag indicating which union case a given instance is.

  1. Struct DUs
[<Struct>]
type DU2 =
    | Case1 of sval: string
    | Case2 of fval: float

This is translated to a flat struct with sval and fval as members, see. https://stackoverflow.com/questions/59738472/struct-attribute-on-discriminated-unions

mgravell commented 4 years ago

This is great, thanks. I'll play with this and see what is possible.