Logo




Subscribe:
RSS 2.0 | Atom 1.0
Categories:

Sign In


[Giagnocavo]Michael::Write()

# Thursday, July 16, 2009
F# Records and ASP.NET MVC Binding

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

        if isrec bc.ModelType then

            let field = bc.ModelType.GetField(propDesc.Name + "@", Reflection.BindingFlags.Instance ||| Reflection.BindingFlags.NonPublic)

            field.SetValue(bc.Model, value)

        else

            base.SetProperty(cc, bc, propDesc, value)

ASP.NET | FSharp
Thursday, July 16, 2009 9:35:53 AM UTC  #    Comments [0]  |  Trackback

OpenID
Please login with either your OpenID above, or your details below.
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):

Live Comment Preview