Monday, April 26, 2010

VS launcher for F# web apps

Sadly, Visual Studio 2010 does not include any F# web application project type.

In practice, this means that if you want to use F# in a web project (be it MVC or WebForms), you have to start with a basic "F# library" project, then manually create a web.config or copy it from somewhere else, manually create a global.asax, global.asax.fs, etc. Or you can let the main web project (i.e. the one with the Global.asax) be a normal C# web app project and reference a F# library where the controllers/webforms are defined.

Another minor annoyance is that you lose the F5 functionality to launch the application, since Visual Studio doesn't know it's a web application. A couple of workarounds for this:

  1. Reference the VS built-in dev web server as the starting external program. This is usually in C:\Program Files (x86)\Common Files\microsoft shared\DevServer\10.0\WebDev.WebServer40.EXE

    fsharp-webdev

    The downside of this is that it doesn't automatically launch your default web browser to your app. It might sound somewhat silly, but when you're used to getting a browser immediately, automatically, it's kind of annoying not having it.

  2. Write a little wrapper around WebServer40.exe that launches a browser. Here's the code:
    open System
    open System.IO
    open System.Diagnostics
    open System.Reflection
    
    [<EntryPoint>]
    let main args =
        // code adapted from FSharp.PowerPack's AspNetTester
        let progfile = 
            let prg = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
            if Environment.Is64BitProcess
                then prg + " (x86)"
                else prg
                
        let webserver = Path.Combine(progfile, @"Common Files\microsoft shared\DevServer\10.0\WebDev.WebServer40.EXE")
        if not (File.Exists webserver)
            then failwith "No ASP.NET dev web server found."
    
        let getArg arg = args |> Seq.tryFind (fun a -> a.ToUpperInvariant().StartsWith arg)
        let webSitePath = 
            match getArg "PATH:" with
            | None -> Directory.GetParent(Directory.GetCurrentDirectory()).FullName
            | Some a -> a.Substring 5
        let port = 
            match getArg "PORT:" with
            | None -> Random().Next(10000, 65535)
            | Some a -> Convert.ToInt32 (a.Substring 5)
        let vpath =
            match getArg "VPATH:" with
            | None -> ""
            | Some a -> a.Substring 6
        let pathArg = sprintf "/path:%s" webSitePath
        let portArg = sprintf "/port:%d" port
        
        let asm = Assembly.LoadFile webserver
        let run (args: string[]) = asm.EntryPoint.Invoke(null, [| args |]) :?> int
    
        Process.Start (sprintf "http://localhost:%d%s" port vpath) |> ignore
        run [| pathArg; portArg |]
    Place the exe (I called it WebStarter.exe) in your web app root, then put it as starting external program:

    fsharp-webdev2

    You can optionally define a fixed port (by default a random port is used), a different root path or a virtual path to start the browser. Set your F# web app project as "Startup Project", hit F5 and voilĂ , browser launches with the debugger hooked up :)

  3. UPDATE: Steve Gilham has another solution, you can just add a couple of elements to the fsproj to turn it into a web app project.
  4. UPDATE: Tomas Petricek created a MVC project template.

7 comments:

Matthew Sullivan said...

You should just create an F# web app template and share it via the gallery or zip it here?

Not sure if what you did with the exe would exactly simply save to a template but I'm sure it could be worked around.

I found the f# templates pretty sparse. But it's pleasantly simple to just save various initial project settings as new templates.

Mauricio Scheffer said...

@Matthew: I agree that a project template would be the right way to do this, but honestly I have no idea about project templates and I'm not interested in learning about them right now. Please let me know if you do create such a template though :)

Steve Gilham said...

Having opened up a C# web project file, I spotted this line in the first <PropertyGroup>

<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

Having managed in the past to turn vanilla C# libraries into workflow projects by transplanting similar Guid tags like this, I suspect putting this in your .fsproj file might make Visual Studio treat that as a proper Web project.

Steve Gilham said...

...continued: There's also a large <ProjectExtensions> (probably a bit big to transcribe here) at the end of the C# file controlling web server settings, which you'd probably also need to transplant.

Mauricio Scheffer said...

@Steve: Tried that, didn't work.

Steve Gilham said...

My bad for shooting from the hip -- the recipe as given doesn't quite work, because I misunderstood the meaning of the project guids. Now I've had a few minutes try try it, I have it working for real.

In the guids clause, GUID fae04ec0-301f-11d3-bf4b-00c04f79efbc means a C# project -- so setting that means your F# project doesn't load -- fails with a "project of the same name already loaded".

Using F2A71F9B-5D33-465A-A702-920D77279786 (which means an F# project as you can see in the .sln file) and the web project GUID 349c5851-65df-11da-9384-00065b846f21 in a ProjectTypeGuid element; adding all the extra references, and copying in the extra <ProjectExtensions> clause from a C# web project gives me an F# project which launches an entirely trivial default.aspx for me in VS2010.

Mauricio Scheffer said...

@Steve: Nice!! I'll give it a try, thanks!