Migrating teamvirtuoso.com to dotnetcore, F# and mssql-server

Eric Polerecky   |   Jan 03, 2017

During the week between Christmas and New Years we decided to migrate teamvirtuoso.com to dotnet core. Previously teamvirtuoso.com was powered by Jekyll, a static site generator that uses Ruby. Using Jekyll allowed me to get teamvirtuoso.com up and running quickly but now that we have time, and a few days off during the holidays, it's time to make our website an example of the work we do.

...with one exception....I wanted to use F#.

We use F# via FAKE for all our build scripts but beyond that we don't build much in F#. Building a mostly static site and a blog should be a good tool for sharpening our F# skills.

Here is the stack we are using for teamvirtuoso.com

  • F#
  • Suave
  • Ubuntu
  • mssql-server
  • Azure Virtual Machine
  • Jenkins for CI/CD

We've deployed applications using Mono, developed Xamarin applications that target iOS and Android for customers but, like everyone else; this is our first time developing and deploying in the brave new cross-platform Microsoft world.

The migration was not without it's bumps but everything seems to be working now. Please let us know if you find an error in the site.

F#

File order in project.json

One of the first things that I learned was that order matters.

Which order? dependency !

"compile": {
		"includeFiles": [
				"DapperFSharp.fs", //dependencies from nuget
				"DotLiquidCore.fs", //dependencies from nuget
				"Form.fs", //no dependencies
				"Posts.fs", //depends on form and dapper
				"Authors.fs", //depends on form and dapper
				"Program.fs" //depends on authors, posts, and dotliquid
		]
}

Also, in C# when you define a function it has an almost global use in that you can call a function that is defined later in a class. However; this won't work in F# because Callee is not defined before it's used. #OrderMatters

 public void Caller(){ 
   var item = Callee();
 }
 
 public string Callee(){
	 return "hello";
 }

dapper

FSharp.Data is not yet ported for dotnet core and since we don't typically do any data access in our FAKE build scripts, we had to learn all about data access in F#. I'm not sure if this is acceptable but since we can call any .NET libraries from F# I decided to pull in Dapper as a way to get up and running without FSharp.Data. Dapper is very light-weight and alreadyand has dotnet core support.

In order to call dapper we had to learn how to use generics in F#. Luckily there is a great gist on calling Dapper from F#.

Cached Results:

One area where I'd like some feedback is method result caching. I'm sure there is an actual name for it; but when a method is called with the same parameters the method is not executed but the results are returned. I believe this is the concept of pure methods. Since the input is the same the output must be the same.

This caused a couple hours of head-ache when working with data access. The list pages don't take in any input so after the first time the results are returned the method was never executed again, even after we added/updated a record in the database.

We resolved this by passing in a new sql connection.


    let getPosts (connection:SqlConnection) =
        let posts = connection.Query<PostAuthor> postAuthorQuery
        posts
				

Please comment if we are way off on how method result caching works in F#.

List and Seq

We write and store the post data in Markdown format. After getting the posts out of the database we need to loop over them all and convert the markdown to HTML. In C# we can do this:

    foreach (var post in posts)
    {
        post.Html = CommonMarkConverter.Convert(post.Markdown);
    }

In order to perform the conversion in F# we use the following (full example so you can see where we use this):


let getPostBySlug slug (connection:SqlConnection) =
    connection
    |> dapperParametrizedQuery<PostAuthor> (postAuthorQuery + " WHERE Slug = @Slug") {Slug=slug}
    |> Seq.toList
    |> List.map (fun value -> {value with Markdown = CommonMark.CommonMarkConverter.Convert(value.PostBody)} )  //this
    |> Seq.head
		

Record types

Record types a basic building block,

Suave

Suave's documentation is wonderful and provided us all the guidance we needed to get started in a new framework and a new (our build scripts are F#) language.

The bulk of the code in Program.fs is:

DotLiquidCore.setTemplatesDir "./public/views/"

let app =
	choose
	[ GET >=> choose
		[ path "/" >=> page "home.liquid" ()
			path "/team" >=> page "team.liquid" ()
			path "/contact" >=> page "contact.liquid" ()
			path "/blog" >=> (getBlog)
			pathScan "/blog/%s" (getPost)
			path "/feed.rss" >=> feed
			//admin section
			....removed....
			//assets
			pathRegex "(.*)\.(css|png|gif|woff|ico|ttf|jpg|js)" >=> Files.browseHome
		]
		POST >=> choose 
		[ path "/admin/posts/create" >=> bindReq (bindForm formPost) createPost BAD_REQUEST
			path "/admin/posts/edit" >=> bindReq (bindForm formPostEdit) updatePost BAD_REQUEST
			path "/admin/authors/create" >=> bindReq (bindForm formAuthor) createAuthor BAD_REQUEST
			path "/admin/authors/edit" >=> bindReq (bindForm formAuthorEdit) updateAuthor BAD_REQUEST
		]
	]

	startWebServer
	{ defaultConfig with             
			homeFolder = Some (Path.GetFullPath "./public") }
	app

0

path

The marketing site is not dynamic, when you send in a path we return a page. These pages don't even have a viewmodel, we we pass in () - The () represents the Type Unit which equates to null in C#.

pathScan

In order to support dynamic uris for the blog; we can use pathScan and pass in a string formating string that is passed to a WebPart. It's really nice that pathScan uses the same syntax as printf. This is the best article we've found about pathScan.

File.browseHome

In order to service static files we use the File.browseHome WebPart.

dotliquid

DotLiquid is a port of the Ruby template engine Liquid. The documentation for DotLiquid is pretty good but what makes working with DotLiquid really nice is that Shopify uses Liquid and their documentation and support is that of a commercial project.

  • http://dotliquidmarkup.org/
  • https://suave.io/dotliquid.html

At the time we started, the nuget packages for Suave didn't have support for dotnet core; however - it looks like it does now - https://github.com/SuaveIO/suave/blob/master/src/Suave.DotLiquid/Library.fs

dotnet core 1.1

When deploying to ubuntu, there is a bug in nuget restore's writing to the /tmp directory that is resolved in 1.1. Upgrading to dotnet core 1.1 only required a small project.json file change.

"frameworks": {
        "netcoreapp1.1": { //change this
            "dependencies": {
                "Microsoft.NETCore.App": {
                    "type": "platform",
                    "version": "1.1.0" //change this
                }
            },
            "imports": "dnxcore50"
        }
    },

In a future post will cover the the linux specific topics of deploying dotnet core on a non-windows platform such as:

  • Jenkins (permissions, cache dir)
  • mssql-server
  • nginx (proxy pass, password protecting folders)
  • supervisor (stopping, deploying)