I suggest that you create one or more Api.fs
files to expose F# code in a C# friendly way.
In this file:
- Define functions with
PascalCase
names. They will appear to C# as static methods. - Functions should use tuple-style declarations (like C#) rather than F#-style params with spaces.
- Also, best to explicitly document the parameters and return types
Example:
module Api
/// Note the tuple-style parameters and PascalCase name
let MyApiMethod(i:int, s:string) :bool =
...
You should check parameters coming from C# as they might be null or otherwise invalid. To throw exceptions you can use:
nullArg paramName
throwsArgumentNullException
invalidArg paramName message
throwsArgumentException
Here's a parameter validation example:
let MyApiMethod(user:LoggedInUser, product:Product, email:string) :bool =
if isNull (box user) then nullArg "user"
if isNull (box product) then nullArg "product"
if isNull email then nullArg "email"
if not(email.Contains("@")) then invalidArg "email" "email must contain @"
// now do something with user and product and email
Note that if a function accepts types that are defined in F# (like LoggedInUser
or Product
in the example above), they can't easily be tested against null
, so we need to use box
to convert them into an object and then test them.
Other types, such as string
, do not need the box
.
Also note the use of if then
without an else
branch. Generally the else
branch is required, but in this case,
since the then
branch returns unit
(because it throws an exception), the else
branch can be omitted.
Records are exposed as readonly classes. No special treatment needed.
- There is a readonly property for each class
- There is a constructor which takes all the fields as parameters.
Note, there is no parameterless constructor available by default. If you need one (eg for serialization) use the CLIMutable
attribute (explanation here).
Unions can be exposed two ways:
- As a new class representing the combined cases. That is, there is a separate field to hold the data for each possible case. See "Unions example 1" below.
- Leave the type alone but expose a "Match" method with a lambda for each case so that the C# code can pattern-match. See "Unions example 2" below.
SCUs used as wrapper types can be exposed two ways:
- By unwrapping them and exposing the underlying primitive string/int, etc
- Otherwise, leave them alone, and they are exposed as a class with
- a single readonly property called
Item
- a static factory method
NewXXX
named after the class which takes one parameter. E.g.ProduceCode.NewProductCode("abc")
- a single readonly property called
- F#
list
should be exposed asIEnumerable
(seq
in F#) usingList.toSeq
- F# sets should (generally) be converted to
IEnumerable
(seq
in F#) usingSet.toSeq
- F# maps should be converted to
IDictionary<_,_>
. This can be done by direct casting (myMap :> IDictionary<_,_>
) or, if you need the performance of a hash table, conversion to a read-only IDictionary (myMap |> Map.toSeq |> dict
) - For mutable collections, use arrays. F# also understands the C#
List<_>
type (calledResizeArray
in F#) but best not to use that in an API.
Optional values can be exposed two ways:
- By using
null
for None (or converting toNullable
).- You can set a F# reference value to
null
usingUnchecked.defaultof<_>
if you have to. See "Unions example 1" below.
- You can set a F# reference value to
- By converting them into a list of 1 or 0 items using
Option.toList
Function can be exposed two ways:
- By exposing them as
Func<>
-- Not recommended in general unless the C# devs are familiar with functional programming. A useful exception to this rule is using them inMatch
methods. See "Unions example 2" below. - By converting them into a interface (See "Functions example" below)
Given a choice type like this:
type PaymentMethod =
| Cash
| Card of CreditCardInfo
| PayPal of EmailAddress
Expose a new class with a field for ALL properties combined and an enum or flag to indicate what case is being used.
type PaymentMethodApi = {
IsCash : bool
IsCard : bool
IsPayPal : bool
Card : CreditCardInfo
PayPal : EmailAddress
}
or you could use an Enum
instead of three flags.
type PaymentMethodCase =
| Cash = 1
| Card = 2
| PayPal = 3
type PaymentMethodApi = {
PaymentMethodCase : PaymentMethodCase
Card : CreditCardInfo
PayPal : EmailAddress
}
To create a value like this, use Unchecked.defaultof<_>
to assign nulls for the fields that are not used.
let convertToApi paymentMethod =
match paymentMethod with
| Cash ->
{
IsCash = true
IsCard = false
IsPayPal = false
CardInfo = Unchecked.defaultof<_>
PayPalInfo = Unchecked.defaultof<_>
}
| Card creditCardInfo ->
{
IsCash = false
IsCard = true
IsPayPal = false
CardInfo = creditCardInfo
PayPalInfo = Unchecked.defaultof<_>
}
| PayPal emailAddress ->
{
IsCash = false
IsCard = false
IsPayPal = true
CardInfo = Unchecked.defaultof<_>
PayPalInfo = emailAddress
}
If the record structure has lots of fields, it's easier to create an "empty" record and then only set the required fields in each case.
let convertToApi paymentMethod =
let empty =
{
IsCash = false
IsCard = false
IsPayPal = false
CardInfo = Unchecked.defaultof<_>
PayPalInfo = Unchecked.defaultof<_>
}
match paymentMethod with
| Cash ->
{ empty with IsCash = true }
| Card creditCardInfo ->
{ empty with IsCard = true; CardInfo = creditCardInfo }
| PayPal emailAddress ->
{ empty with IsPayPal = true; PayPalInfo = emailAddress }
The other approach is to leave the F# choice type alone but also export a "match" method with a lambda for each case so that the C# code can pattern match against the cases in the same way that F# does.
/// exposed in API, hence the use of PascalCase
/// and Tuple-style parameters
let Match<'T>
(
(paymentMethod : PaymentMethod),
(handleCash : System.Func<'T>),
(handleCard : System.Func<CreditCardInfo,'T>),
(handlePayPal : System.Func<EmailAddress,'T>)
) =
match paymentMethod with
| Cash ->
handleCash.Invoke()
| Card cardInfo ->
handleCard.Invoke(cardInfo)
| PayPal emailAddress ->
handlePaypal.Invoke(emailAddress)
The C# code then looks like:
var paymentMethod = this.Get....
Api.Match<int>(
paymentMethod,
handleCash: () => {
return 0;
},
handleCard: cardInfo => {
return 1;
},
handlePayPal: emailAddress => {
return 2;
}
);
Interfaces are more familiar than stand-alone functions in C#, so providing a one-method interface rather than a Func<_>
will make the code easier to use. It's straightforward to convert between F# functions and interfaces, as shown in the examples below.
/// Declare a one-parameter F# function type
type ConvertToString = int -> string
/// Declare an equivalent one-parameter method on an interface
type IConvertToString =
abstract Execute : int -> string
/// Declare a two-parameter F# function type
type Adder = int -> int -> int
/// Declare an equivalent two-parameter method on an interface
/// Note the use of "tuple" signature style
type IAdder =
abstract Execute : int * int -> int
Here's an implementation of the Adder function type in F#. The parameters are constrained by the definition.
let adder : Adder =
fun i j -> i + j
With these defined, we can convert between them:
/// Accept an IAdder interface from C# and
/// convert it to an internal F# function
let ApiMethod(adder:IAdder) =
let fsharpAdder i j = // declare an F#-friendly function
adder.Execute(i,j) // call the interface that was passed in
// do something with fsharpAdder
()
We can also go the other way and create an interface from an F# function:
/// Create an IAdder interface from an internal F# function
let NewIAdder() : IAdder =
// this "object expression" syntax implements an interface without having to define a whole class
{new IAdder with
member this.Execute(i,j) = adder i j // call the internal F# function
}