2 people like it.

C#-Friendly Single-Case Discriminated Unions

Single-case Discriminated Unions (https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) are a lightweight way to create types for important concepts in your domain. Unfortunately, there's a few gotchas when consuming from C# to be aware of. For one, if you use the `==` or `!=` operators in C#, it will do a reference comparison, which is almost certainly not what you want for these wrapper types. By overriding `op_Equality` and `op_Inequality`, this will force it to use a structural comparison. Secondly, when using string concatenation via `+`, `String.Format`, or string interpolation, C# will implicitly convert non-string arguments to a string via `ToString`. This means if you forget to use `.Item` to unwrap your value, you will not get a compiler error, it will just implicitly call `ToString`, which by default will look like this: `Id "c148b684-2c40-4383-a1b9-0e8f37752fd0"`. By overriding `ToString`, we can make sure it will look like the raw underlying type when converted to a string: `c148b684-2c40-4383-a1b9-0e8f37752fd0`.

type Id =
    Id of System.Guid
        override this.ToString() = let (Id id) = this in string id
        static member op_Equality (a, b : Id) = a = b
        static member op_Inequality (a, b : Id) = a <> b
Multiple items
union case Id.Id: System.Guid -> Id

type Id =
  | Id of Guid
  override ToString : unit -> string
  static member ( = ) : a:Id * b:Id -> bool
  static member ( <> ) : a:Id * b:Id -> bool

Full name: Script.Id
namespace System
Multiple items
type Guid =
    new : b:byte[] -> Guid + 4 overloads
    member CompareTo : value:obj -> int + 1 overload
    member Equals : o:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member ToByteArray : unit -> byte[]
    member ToString : unit -> string + 2 overloads
    static val Empty : Guid
    static member NewGuid : unit -> Guid
    static member Parse : input:string -> Guid
    static member ParseExact : input:string * format:string -> Guid

Full name: System.Guid

System.Guid(b: byte []) : unit
System.Guid(g: string) : unit
System.Guid(a: int, b: int16, c: int16, d: byte []) : unit
System.Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : unit
System.Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : unit
val this : Id
override Id.ToString : unit -> string

Full name: Script.Id.ToString
val id : System.Guid
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

type string = System.String

Full name: Microsoft.FSharp.Core.string
val a : Id
val b : Id
Raw view Test code New version

More information

Posted:4 years ago
Author:Justin Hewlett
Tags: c# , discriminated union , domain , domain modelling , interop