I came across a thread on hubFS about deserialising F# records with JSON. After Mr. McNamara pointed out that DataContract serialization would work with F# records, I realised we can do the same for other serialisation systems, such as ASP.NET MVC.
ASP.NET MVC binding doesn't work with F# records for a few reasons. First, it requires a default constructor, and record types don't have one. Second, it needs settable properties, and records have read-only properties. Fortunately, the backing field for a record's property is a mutable field. The name is mangled (@ is appended), but otherwise we're ok to set that field.
With this, we can subclass the default model binder and add in code to construct records as well as set their fields directly. Unlike DataContract serializers, I didn't use FormatterServices.GetUninitializedObject to create the object, I use the F# reflection function MakeRecord. This is because I want to attempt to initialise all fields on the record type, to try to keep out nulls. This goes against how the rest of MVC's null handling goes, so perhaps it's not a great idea.
At any rate, here's the quite short code. A lot of things probably don't work, such as F# lists. Perhaps there should be a community project that collects F#-specific type helpers for different frameworks to make serialization, binding, etc. easier.
open System
open System.Web.Mvc
open Microsoft.FSharp.Reflection
type RecordDefaultModelBinder() =
inherit DefaultModelBinder()
let isrec = FSharpType.IsRecord
/// Makes a record, trying to provide initialised values for each field
let rec makeDefaultRecord ty =
let defval ty =
if isrec ty then makeDefaultRecord ty
else match ty.GetConstructor(Type.EmptyTypes) with null -> null | c -> c.Invoke null
let vals = FSharpType.GetRecordFields ty |> Array.map (fun x -> defval x.PropertyType)
FSharpValue.MakeRecord(ty, vals)
override this.CreateModel(cc, bc, ty) =
// We have to avoid them calling Activator.CreateInstance on records
if isrec ty then makeDefaultRecord(ty) else base.CreateModel(cc, bc, ty)
override this.GetModelProperties(cc, bc) =
// Default one filters out read-only, but we own the field
if isrec bc.ModelType then
let props = ComponentModel.TypeDescriptor.GetProperties(bc.ModelType)
|> Seq.cast<ComponentModel.PropertyDescriptor> // BCLFail
|> Seq.filter(fun p -> bc.PropertyFilter.Invoke(p.Name))
ComponentModel.PropertyDescriptorCollection(Seq.to_array props)
else
base.GetModelProperties(cc, bc)
override this.SetProperty(cc, bc, propDesc, value) =
// To set a record property, set the mangled field
let field = bc.ModelType.GetField(propDesc.Name + "@", Reflection.BindingFlags.Instance ||| Reflection.BindingFlags.NonPublic)
field.SetValue(bc.Model, value)
base.SetProperty(cc, bc, propDesc, value)
Remember Me