Sunday, May 16, 2010

F# and the Standard ASP.NET MVC 2 Web Application Template

Tomas Petricek recently provided a great blog post on creating MVC web applications in F#.  I thought it might be nice to also have a template that replicates the functionality of the standard C# ASP.NET MVC 2 Web Application template. The primary aspects of this template that differ from Tomas's include custom validators, authentication and membership services, and several jQuery plugins that are recommended and frequently used by Elijah Manor.

Here are a few examples of some of the code differences.

Custom Validators:
namespace FSharpMVC2.Web.Models
open System
open System.ComponentModel.DataAnnotations
open System.ComponentModel
open System.Globalization
open System.Web.Security

[<AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)>]
[<Sealed>] 
type PropertiesMustMatchAttribute = 
    inherit ValidationAttribute 
        val typeId : obj
        val mutable originalProperty : string
        val mutable confirmProperty : string
        new (originalProperty, confirmProperty) = 
            {inherit ValidationAttribute("'{0}' and '{1}' do not match."); originalProperty = originalProperty; 
             confirmProperty = confirmProperty; typeId = new Object()} 
        member x.OriginalProperty with get() = x.originalProperty and set(value) = x.originalProperty <- value
        member x.ConfirmProperty with get() = x.confirmProperty and set(value) = x.confirmProperty <- value
        override x.TypeId with get() = x.typeId
        override x.FormatErrorMessage name =
            String.Format(CultureInfo.CurrentUICulture, x.ErrorMessageString, x.OriginalProperty, x.ConfirmProperty)
        override x.IsValid value =
            let properties = TypeDescriptor.GetProperties value
            let originalValue = properties.Find(x.OriginalProperty, true).GetValue(value)
            let confirmValue = properties.Find(x.ConfirmProperty, true).GetValue(value)
            Object.Equals(originalValue, confirmValue)

[<AttributeUsage(AttributeTargets.Field ||| AttributeTargets.Property, AllowMultiple = false, Inherited = true)>]
[<Sealed>] 
type ValidatePasswordLengthAttribute = 
    val minCharacters : int
    new () = {inherit ValidationAttribute("'{0}' must be at least {1} characters long."); 
                minCharacters = Membership.Provider.MinRequiredPasswordLength} 
    inherit ValidationAttribute 
        override x.FormatErrorMessage name =
            String.Format(CultureInfo.CurrentUICulture, x.ErrorMessageString, name, x.minCharacters)
        override x.IsValid value =
            let valueAsString = value :?> string
            (valueAsString <> null && valueAsString.Length >= x.minCharacters)
Authentication and Membership Services:
namespace FSharpMVC2.Web.Models

open System
open System.Web.Security

module InputValidation =
    let Validate stringValue errorMessage =
        match String.IsNullOrEmpty(stringValue) with
        | true -> failwith(errorMessage)
        | _ -> do "" |> ignore

type IMembershipService = interface
    abstract MinPasswordLength : int with get
    abstract ValidateUser : string*string -> bool
    abstract CreateUser : string*string*string -> MembershipCreateStatus
    abstract ChangePassword : string*string*string -> bool
end    

type AccountMembershipService =
    val provider : MembershipProvider
    new () = AccountMembershipService(Membership.Provider)
    new (provider) = {provider = provider}
    interface IMembershipService with 
        member x.MinPasswordLength with get() = x.provider.MinRequiredPasswordLength
        member x.ValidateUser (userName, password) =
            InputValidation.Validate userName "Username cannot be null or empty."
            InputValidation.Validate password "Password cannot be null or empty."
            x.provider.ValidateUser(userName, password)
        member x.CreateUser (userName, password, email) =
            InputValidation.Validate userName "Username cannot be null or empty."
            InputValidation.Validate password "Password cannot be null or empty."
            InputValidation.Validate email "Email cannot be null or empty."
            let (_, status) = x.provider.CreateUser(userName, password, email, null, null, true, null)
            status
        member x.ChangePassword(userName, oldPassword, newPassword) =
            InputValidation.Validate userName "Username cannot be null or empty."
            InputValidation.Validate oldPassword "OldPassword cannot be null or empty."
            InputValidation.Validate newPassword "NewPassword cannot be null or empty."            
            try
                let currentUser = x.provider.GetUser(userName, true)
                currentUser.ChangePassword(oldPassword, newPassword)
            with 
            | :? ArgumentException -> false
            | :? MembershipPasswordException -> false

type IFormsAuthenticationService =
    abstract SignIn : string*bool -> unit
    abstract SignOut : unit -> unit

type FormsAuthenticationService() =
    interface IFormsAuthenticationService with
        member x.SignIn(userName, createPersistentCookie) =
            InputValidation.Validate userName "Username cannot be null or empty"
            do FormsAuthentication.SetAuthCookie(userName, createPersistentCookie)
        member x.SignOut () =
            do FormsAuthentication.SignOut()
You can download the template installer by clicking here.  The full solution can be found at http://github.com/dmohl/FSharpMVC2Starter.  (Note: The full solution includes a few things that are not included in the template such as a sample test project for the home controller.)

3 comments: