├── .gitignore ├── .paket └── paket.bootstrapper.exe ├── LICENSE ├── README.md ├── dox ├── content │ ├── index.md │ ├── step1 │ │ ├── dos.md │ │ ├── index.md │ │ ├── quatro.fsx │ │ ├── tres.fsx │ │ └── uno.md │ ├── step2 │ │ ├── index.md │ │ ├── quatro.fsx │ │ ├── tres.fsx │ │ └── uno-dos.md │ └── step3 │ │ ├── dos.md │ │ ├── index.md │ │ ├── quatro.fsx │ │ ├── tres.fsx │ │ └── uno.md ├── files │ └── img │ │ ├── logo-template.pdn │ │ └── logo.png └── tools │ ├── generate.fsx │ └── templates │ └── template.cshtml ├── paket.dependencies ├── paket.lock └── src ├── 1-AspNetCore-CSharp ├── App.cs ├── Data │ ├── DataConfig.cs │ ├── EnvironmentExtensions.cs │ └── Table.cs ├── Entities │ ├── Authorization.cs │ ├── AuthorizationLevel.cs │ ├── Category.cs │ ├── Comment.cs │ ├── CommentStatus.cs │ ├── Page.cs │ ├── Post.cs │ ├── PostStatus.cs │ ├── Revision.cs │ ├── User.cs │ └── WebLog.cs ├── Startup.cs ├── Uno.csproj └── appsettings.json ├── 2-Nancy-CSharp ├── App.cs ├── Data │ ├── DataConfig.cs │ ├── EnvironmentExtensions.cs │ └── Table.cs ├── Dos.csproj ├── DosBootstrapper.cs ├── Entities │ ├── Authorization.cs │ ├── AuthorizationLevel.cs │ ├── Category.cs │ ├── Comment.cs │ ├── CommentStatus.cs │ ├── Page.cs │ ├── Post.cs │ ├── PostStatus.cs │ ├── Revision.cs │ ├── User.cs │ └── WebLog.cs ├── Modules │ └── HomeModule.cs ├── Startup.cs └── data-config.json ├── 3-Nancy-FSharp ├── App.fs ├── Data.fs ├── Entities.fs ├── Extensions.fs ├── HomeModule.fs ├── Tres.fsproj └── data-config.json ├── 4-Freya-FSharp ├── App.fs ├── Data.fs ├── Dependencies.fs ├── Entities.fs ├── Extensions.fs ├── Quatro.fsproj └── data-config.json └── FromObjectsToFunctions.sln /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | # Documentation output 255 | dox/output/ 256 | -------------------------------------------------------------------------------- /.paket/paket.bootstrapper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljsummers/FromObjectsToFunctions/0b67eb49b727ed12a8c1016bfa11c59b717dc8e3/.paket/paket.bootstrapper.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Daniel J. Summers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # objects () |> functions 2 | 3 | ## THIS REPOSITORY IS DEPRECATED 4 | 5 | **New Repository:** https://github.com/bit-badger/o2f 6 | 7 | **New Website:** https://objects-to-functions.bitbadger.solutions 8 | 9 | The code in this repository targets .Net Core version 1, with `project.json` and using packages that were, at the time, in pre-release status. Additionally, the company behind [RethinkDB](https://www.rethinkdb.com) shuttered, and while it was transferred to the Linux Foundation and is open source, its development has slowed substantially. There are a few branches where updates to .Net Core 2 and [RavenDB](https://ravendb.net) were targeted, but those are incomplete. Finally, since the original development, [Giraffe](https://github.com/giraffe-fsharp/Giraffe) was released as a functional way to do ASP.NET Core development. 10 | 11 | The linked repository and site above can be thought of as "version 2" of this effort, although the name has changed slightly. It also contains five repositories: 12 | 13 | 1. ASP.NET Core MVC / C# (**Uno**) - The C# standard 14 | 15 | 2. Nancy / C# (**Dos**) - A popular alternative 16 | 17 | 3. Nancy / F# (**Tres**) - A C#-to-F# translation of **Dos** 18 | 19 | 4. Giraffe / F# (**Quatro**) - Part translation of **Uno**, part evolving to more idiomatic F# 20 | 21 | 5. Freya / F# (**Cinco**) - Fully-functional implementation using very few libraries 22 | 23 | _(This README update is the final update that will occur in this repository; if you have starred this repository and want to continue to track its development, please star [the new repository](https://github.com/bit-badger/o2f) instead.)_ 24 | -------------------------------------------------------------------------------- /dox/content/index.md: -------------------------------------------------------------------------------- 1 | # objects () |> functions 2 | 3 | This page serves / will serve as the table of contents for each step of the development process. Below the TOC is a table with comparisons among the 4 projects. 4 | 5 | **[Step 1](step1)** - Hello World 6 | Establish a web request response with the least possible / complex code 7 | 8 | **[Step 2](step2)** - Data Model 9 | Create the persistent types we'll use for our application 10 | 11 | **[Step 3](step3)** - RethinkDB Connection 12 | Configure and prepare a RethinkDB connection for use in our application 13 | 14 | **Step 4** - Framework Setup 15 | Create folders / conventions for our application and load some dummy data 16 | 17 | **Step 5** - Routes 18 | Establish the URLs that our application will recognize 19 | 20 | **Step 6** - Views 21 | Implement the web pages by which our application will display its information 22 | 23 | **Step 7** - Log In 24 | Users are important in this application! 25 | 26 | **Step 8** - Page Publishing and Editing 27 | Create and edit pages 28 | 29 | **Step 9** - Post Publishing and Editing 30 | Finally, we can write some posts! 31 | 32 | **Step 10** - Categories and Tags 33 | Handle lists of posts by category or tag 34 | 35 | **Step 11** - RSS Feeds 36 | Provide RSS and Atom feeds for our content 37 | 38 | **Step n** - may be added later (yay, scope creep!) 39 | 40 | | item | Uno | Dos | Tres | Quatro | 41 | | --- | --- | --- | --- | --- | 42 | | Language | C# | C# | F# | F# | 43 | | Server | Kestrel | Kestrel | Kestrel | Suave | 44 | | Framework | ASP.NET Core | Nancy | Nancy | Freya | 45 | | Views | Razor | SSVE | SSVE | SSVE | 46 | -------------------------------------------------------------------------------- /dox/content/step1/dos.md: -------------------------------------------------------------------------------- 1 | ### Dos - Step 1 2 | 3 | For this project, we'll make sure our project file is `Dos.csproj`, and modify it the way we did [for Uno](./uno.html); we'll include one extra dependency to bring in Nancy. 4 | 5 | [lang=text] 6 | 7 | Dos 8 | 1.0.0 9 | Exe 10 | netcoreapp2.0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Nancy strives to provide a Super-Duper-Happy-Path (SDHP), where all you have to do is follow their conventions, and everything will "just work." (You can also configure every aspect of it; it's only opinionated in its defaults.) One of these conventions is that the controllers inherit from `NancyModule`, and when they do, no further configuration is required. So, we create the `Modules` directory, and add `HomeModule.cs`, which looks like this: 20 | 21 | [lang=csharp] 22 | namespace Dos.Modules 23 | { 24 | using Nancy; 25 | 26 | public class HomeModule : NancyModule 27 | { 28 | public HomeModule() : base() 29 | { 30 | Get("/", _ => "Hello World from Nancy C#"); 31 | } 32 | } 33 | } 34 | 35 | Since we'll be hosting this with Kestrel (via OWIN), we still need a `Startup.cs`, though its `Configure()` method looks a bit different: 36 | 37 | [lang=csharp] 38 | public void Configure(IApplicationBuilder app) => 39 | app.UseOwin(x => x.UseNancy()); 40 | 41 | (We need to add a using statement for `Nancy.Owin` so that the `UseNancy()` method is visible.) 42 | 43 | The `App.cs` file is identical to the one from Uno. 44 | 45 | [Back to Step 1](../step1) -------------------------------------------------------------------------------- /dox/content/step1/index.md: -------------------------------------------------------------------------------- 1 | ## Hello World 2 | 3 | For "Hello World" in each environment, we'll create an empty .NET Core console application. To view the entire 4 | completed source from this step, see 5 | [the checkpoint for step 1](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-1-core2). 6 | 7 | All projects were created using `dotnet new -t console -l C#` (or `F#`). 8 | 9 | **Uno** - [In Depth](uno.html) 10 | 11 | **Dos** - [In Depth](dos.html) 12 | 13 | **Tres** - [In Depth](tres.html) 14 | 15 | **Quatro** - [In Depth](quatro.html) -------------------------------------------------------------------------------- /dox/content/step1/quatro.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Freya.Core/lib/net452/Freya.Core.dll" 3 | #r "../../../packages/Freya.Machines.Http/lib/net452/Freya.Machines.Http.dll" 4 | #r "../../../packages/Freya.Routers.Uri.Template/lib/net452/Freya.Routers.Uri.Template.dll" 5 | #r "../../../packages/Freya.Types.Uri.Template/lib/net452/Freya.Types.Uri.Template.dll" 6 | #r "../../../packages/Microsoft.AspNetCore.Hosting/lib/net451/Microsoft.AspNetCore.Hosting.dll" 7 | #r "../../../packages/Microsoft.AspNetCore.Hosting.Abstractions/lib/net451/Microsoft.AspNetCore.Hosting.Abstractions.dll" 8 | #r "../../../packages/Microsoft.AspNetCore.Http.Abstractions/lib/net451/Microsoft.AspNetCore.Http.Abstractions.dll" 9 | #r "../../../packages/Microsoft.AspNetCore.Owin/lib/net451/Microsoft.AspNetCore.Owin.dll" 10 | #r "../../../packages/Microsoft.AspNetCore.Server.Kestrel/lib/net451/Microsoft.AspNetCore.Server.Kestrel.dll" 11 | 12 | (** 13 | ### Quatro - Step 1 14 | 15 | Having [already made the leap to F#](./tres.html), we will now do our Hello World in Freya. Thanks to the hard work of 16 | Microsoft on .NET Core 2, this process exactly mirrors what we did with Tres, just with a Freya dependency instead of 17 | one for Nancy: 18 | 19 | [lang=text] 20 | 21 | 22 | 23 | 24 | 25 | 26 | We'll go ahead and rename `Program.fs` to `App.fs` to remain consistent among the projects, and tell the compiler about 27 | it: 28 | 29 | [lang=text] 30 | 31 | 32 | 33 | 34 | Now, let's actually write `App.fs`: 35 | *) 36 | namespace Quatro 37 | 38 | open Freya.Core 39 | open Freya.Machines.Http 40 | open Freya.Routers.Uri.Template 41 | open Microsoft.AspNetCore.Builder 42 | open Microsoft.AspNetCore.Hosting 43 | (** 44 | `Freya.Core` gives us the `freya` computation expression, which we will use for the main part of our request handling. 45 | `Freya.Machines.Http` provides the `freyaMachine` computation expression, which allows us to define our 46 | request-response. `Freya.Routers.Uri.Template` provides the `freyaRouter` computation expression, where we assign an 47 | HTTP machine to a URL route pattern. 48 | 49 | Continuing on... 50 | *) 51 | module App = 52 | let hello = 53 | freya { 54 | return Represent.text "Hello World from Freya" 55 | } 56 | 57 | let machine = 58 | freyaMachine { 59 | handleOk hello 60 | } 61 | 62 | let router = 63 | freyaRouter { 64 | resource "/" machine 65 | } 66 | (** 67 | This code uses the three expressions described above to define the response (hard-coded for now), the machine that uses 68 | it for its OK response, and the route that uses the machine. 69 | 70 | Still within `module App =`... 71 | *) 72 | type Startup () = 73 | member __.Configure (app : IApplicationBuilder) = 74 | let freyaOwin = OwinMidFunc.ofFreya (UriTemplateRouter.Freya router) 75 | app.UseOwin (fun p -> p.Invoke freyaOwin) |> ignore 76 | 77 | [] 78 | let main _ = 79 | use host = (new WebHostBuilder()).UseKestrel().UseStartup().Build() 80 | host.Run() 81 | 0 82 | (** 83 | This is the familiar `Startup` class from Tres, except that the `Configure()` method uses the Freya implementation 84 | instead of the Nancy implementation. Notice that the middleware function uses the router as the hook into the 85 | pipeline; that is how we get the OWIN request to be handled by Freya. Notice how much closer to idiomatic F# this code 86 | has become; the only place we had to `ignore` anything was the "seam" where we interoperated with the OWIN library. 87 | 88 | `dotnet run` should succeed at this point, and localhost:5000 should display our Hello World message. 89 | 90 | [Back to Step 1](../step1) 91 | *) -------------------------------------------------------------------------------- /dox/content/step1/tres.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Nancy/lib/net452/Nancy.dll" 3 | #r "../../../packages/Microsoft.AspNetCore.Hosting/lib/net451/Microsoft.AspNetCore.Hosting.dll" 4 | #r "../../../packages/Microsoft.AspNetCore.Hosting.Abstractions/lib/net451/Microsoft.AspNetCore.Hosting.Abstractions.dll" 5 | #r "../../../packages/Microsoft.AspNetCore.Http.Abstractions/lib/net451/Microsoft.AspNetCore.Http.Abstractions.dll" 6 | #r "../../../packages/Microsoft.AspNetCore.Owin/lib/net451/Microsoft.AspNetCore.Owin.dll" 7 | #r "../../../packages/Microsoft.AspNetCore.Server.Kestrel/lib/net451/Microsoft.AspNetCore.Server.Kestrel.dll" 8 | 9 | (** 10 | ### Tres - Step 1 11 | 12 | Here, we're making the leap to F#. Once we ensure that our project file is named `Tres.fsproj`, the contents of the 13 | file should be the same as they were for [Dos](./dos.html). F# projects are historically not split into directories, as 14 | compilation order is significant, and having them in the same directory allows the tooling to ensure that the 15 | compilation order is preserved. With the structure of the `.fsproj` file, this is not necessarily a limitation (though 16 | the tooling still doesn't support it, as of this writing), but we'll follow it for our purposes here. 17 | 18 | The module is created as `HomeModule.fs` in the project root: 19 | *) 20 | namespace Tres 21 | 22 | open Nancy 23 | 24 | type HomeModule() as this = 25 | inherit NancyModule() 26 | 27 | do 28 | this.Get("/", fun _ -> "Hello World from Nancy F#") 29 | (** 30 | If you look at [Dos](./dos.html), you can see how the translation occurred: 31 | 32 | - "using" becomes "open" 33 | - F# does not express constructors in the way C# folks are used to seeing them. Parameters to the class are specified 34 | in the type declaration (or a `new` function, which we don't need for our purposes), and then are visible throughout 35 | the class. 36 | - Since we don't have an explicit constructor where we can wire up the `Get()` method call, we accomplish it using a 37 | `do` binding; this is code that will be run every time the class is instantiated. The `as this` at the end of 38 | `type HomeModule()` allows us to use `this` to refer to the current instance; otherwise, `do` cannot see it. 39 | - This also illustrates the syntax differences in defining lambdas between C# and F#. F# uses the `fun` keyword to 40 | indicate an anonymous function. The `_` is used to indicate that we do not care what the parameter is; since this 41 | request doesn't require anything from the `DynamicDictionary` Nancy provides, we don't. 42 | 43 | We rename `Program.fs` to `App.fs`, and in this file, we provide the contents from both `Startup.cs` and `App.cs`. 44 | *) 45 | namespace Tres 46 | 47 | open Microsoft.AspNetCore.Builder 48 | open Microsoft.AspNetCore.Hosting 49 | open Nancy 50 | open Nancy.Owin 51 | 52 | type Startup() = 53 | member this.Configure (app : IApplicationBuilder) = 54 | app.UseOwin (fun x -> x.UseNancy (fun x -> ()) |> ignore) |> ignore 55 | 56 | module App = 57 | [] 58 | let main argv = 59 | use host = (new WebHostBuilder()).UseKestrel().UseStartup().Build() 60 | host.Run() 61 | 0 62 | (** 63 | The `Startup` class is exactly the same as the C# version, though it appears much differently. The `UseNancy()` method 64 | returns quite a complex result, but the parameter to the `UseOwin()` method expects an `Action<>`; by definition, this 65 | returns `void`\*. In F#, there is no implicit throwaway of results\**; you must explicitly mark results that should be 66 | ignored. `UseNancy` also expects an `Action<>`, so we end up with an extra lambda and two `ignore`s to accomplish the 67 | same thing. 68 | 69 | The `App` module is also new. F# modules can be thought of as static classes (if you use one from C#, that's what they 70 | look like). An F# source file must start with either a namespace or module declaration; also, any code (`let`, `do`, 71 | `member`, etc.) cannot be simply in a namespace. We start with the `Tres` namespace so that our `Startup` class's full 72 | name will be `Tres.Startup`, so we have to define a module for our `let` binding / entry point. 73 | 74 | At this point, `dotnet build` will fail. I mentioned compilation order earlier; we've added one file and renamed the 75 | other, but we have yet to tell the compiler about them, or how they should be ordered. Back in `Tres.fsproj`, between 76 | the `PropertyGroup` and the `ItemGroup`, add the following `ItemGroup`: 77 | 78 | [lang=text] 79 | 80 | 81 | 82 | 83 | 84 | (In the future, we'll add updating this list to our discipline of creating a new file.) 85 | 86 | Now, we can execute `dotnet run`, watch it start, visit localhost:5000, and see our F# message. 87 | 88 | [Back to Step 1](../step1) 89 | 90 | --- 91 | 92 | \* The `unit` type in F# is the parallel to this, but there's more to it than just "something else to call `void`." 93 | 94 | \** For example, `StringBuilder.Append()` returns the builder so you can chain calls, but it also mutates the builder, 95 | and you don't have to provide a variable assignment for every call. In F#, you would either need to provide that, or 96 | pipe the output (`|>`) to `ignore`. 97 | *) -------------------------------------------------------------------------------- /dox/content/step1/uno.md: -------------------------------------------------------------------------------- 1 | ### Uno - Step 1 2 | 3 | _NOTE: While there is a "web" target for C#, it pulls in a lot of files that I'd rather not go through and explain. We 4 | will not be using Entity Framework for anything, and though this application will use some of the Identity features of 5 | ASP.NET Core MVC, we will not be using its membership features. Since all of that is out of scope for this effort, and 6 | all of this is in the "web" template, we won't use it._ 😃 7 | 8 | To start, we'll make sure the `.csproj` file is named `Uno.csproj`. Then, under the first `PropertyGroup` item, we'll add a few items; when we're done, it should look like this: 9 | 10 | [lang=text] 11 | 12 | Uno 13 | 1.0.0 14 | Exe 15 | netcoreapp2.0 16 | 17 | 18 | Then, we'll add a new section, `ItemGroup`, and two dependencies: 19 | 20 | [lang=text] 21 | 22 | 23 | 24 | 25 | 26 | `dotnet restore` fixes up the actual packages. Next, we'll create the `Startup.cs` file. Within its `Configure` method, we'll do a very basic lambda to return a string: 27 | 28 | [lang=csharp] 29 | public void Configure(IApplicationBuilder app) => 30 | app.Run(async context => await context.Response.WriteAsync("Hello World from ASP.NET Core")); 31 | 32 | (We put in using statements for `Microsoft.AspNetCore.Builder` to make the `IApplicationBuilder` visible and `Microsoft.AspNetCore.Http` to expose the `WriteAsync()` method on the `Response` object.) 33 | 34 | We'll rename `Program.cs` to `App.cs`. (Why? Well - why not?) Then, within the `Main()` method, we'll construct a Kestrel instance and run it. 35 | 36 | [lang=csharp] 37 | using (var host = new WebHostBuilder().UseKestrel().UseStartup().Build()) 38 | { 39 | host.Run(); 40 | } 41 | 42 | (Most demos don't show the web host wrapped in a using block; it's `IDisposable`, though, so it's a good idea.) 43 | 44 | At this point, `dotnet run` should give us a successful startup, and browsing to localhost:5000 returns our greeting. 45 | 46 | [Back to Step 1](../step1) -------------------------------------------------------------------------------- /dox/content/step2/index.md: -------------------------------------------------------------------------------- 1 | ### Data Model 2 | 3 | _(Feel free to browse 4 | [the checkpoint for step 2](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-2-core2) as you follow 5 | along.)_ 6 | 7 | #### Overview 8 | 9 | For our data model, we will begin with how we will store it. At a high level: 10 | 11 | - Web logs have a name, an optional subtitle, a theme, a URL, and a time zone 12 | - Users have an e-mail address, a password, a first name, a last name, a preferred name, and a personal URL 13 | - Categories have a name, a URL-friendly "slug", and a description 14 | - Posts have a title, a status, a permalink, when they were published and last updated, 0 or more tags, the text of the 15 | post, and a list of revisions of that post 16 | - Pages have a title, a permalink, when they were published and last updated, whether they should show in the default 17 | page list (think "About", "Contact", etc.), the text of the page, and a list of revisions to that page 18 | - Comments have a name, an e-mail address, an optional URL, a status, when they were posted, and the text of the 19 | comment 20 | 21 | As far as relationships among these entities: 22 | 23 | - Users can have differing authorization levels among the different web logs to which they are authorized 24 | - Categories, Posts, and Pages all each belong to a specific web log 25 | - Comments belong to a specific Post 26 | - Posts are linked to the user who authored them 27 | - Categories can be nested (parent/child) 28 | - Comments can be marked as replies to another comment 29 | - Posts can be assigned to multiple Categories (and can have multiple Comments, as implied above) 30 | - Revisions (Posts and Pages) will track the date/time of the revision and the text of the post or page as of that time 31 | 32 | Both Uno and Dos will use the same C# model. For Tres, we'll convert classes to F# record types (and `null` checks to 33 | `Option`s). For Quatro, we'll make some concrete types for some of these primitives, making it more difficult to 34 | represent an invalid state within our model. (We'll also deal with the implications of those in step 3.) 35 | 36 | #### Implementation Notes 37 | 38 | Our C# data model looks very much like one you'd see in an Entity Framework project. The major difference is that what 39 | would be the navigation properties; collections (ex. the `Revisions` collection in the `Page` and `Post`) are part of 40 | the type, rather than a `Revision` being its own entity, while parent navigation properties (ex. `WebLog` for entities 41 | that define a `WebLogId` property) do not exist. Even if you are unfamiliar with Entity Framework, you will likely 42 | easily see how this model could be represented in a relational database. 43 | 44 | Some other design decisions: 45 | 46 | - We will use strings (created from `Guid`s) as our Ids for entities 47 | - Authorization levels, post statuses, and comment statuses are represented as strings, but we provide a means to avoid 48 | magic strings in the code while dealing with these 49 | - Properties representing date/time will be stored as `long`/`int64`, representing ticks. _(We'll use NodaTime for 50 | manipulation, but this would also support using something built-in like `DateTime.UtcNow.Ticks`.)_ 51 | - While technically a part of step 3, we will annotate all `Id` fields with the `JsonProperty("id")` attribute. 52 | RethinkDB will generate an `id` field for documents that do not have one, using it as the primary key, and this check 53 | is done case-sensitively. Without this attribute, we would have to define a secondary index for our primary key, which 54 | wouldn't make much sense at all. 55 | 56 | #### Project-Specific Notes 57 | 58 | **Uno / Dos** - [In Depth](uno-dos.html) 59 | 60 | **Tres** - [In Depth](tres.html) 61 | 62 | **Quatro** - [In Depth](quatro.html) -------------------------------------------------------------------------------- /dox/content/step2/quatro.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll" 3 | 4 | namespace Quatro.Entities 5 | 6 | open Newtonsoft.Json 7 | 8 | type Revision = { 9 | AsOf : int64 10 | Text : string 11 | } 12 | with 13 | static member Empty = 14 | { AsOf = 0L 15 | Text = "" 16 | } 17 | (** 18 | ### Quatro - Step 2 19 | 20 | As we [converted our model to F#](tres.html), we brought in some immutability. What we created can form the basis of a 21 | fully operational F# application; but we can do better. For example, given "faad05f6-c539-44b7-9e94-1b68da4bba57" - 22 | quick! Is it a post Id? A page Id? The text of a really lame blog post? Also, what is to prevent us from using a 23 | `CommentStatus` value in a spot where a `PostStatus` should go? (Do you really want your own post to be able to be 24 | flagged as spam?) 25 | 26 | To be sure, these same problems exist in most OO realms, and developers manage to keep all the strings separate. 27 | However, just as immutability gets rid of `null` checks, F# has features that go even further, and can help us create a 28 | model where invalid states cannot be represented. _F# for Fun and Profit_ has a great series on 29 | [Designing with Types](http://fsharpforfunandprofit.com/series/designing-with-types.html), and I highly recommend 30 | reading it; it goes into way more depth that we're going to at this point. 31 | 32 | The language feature we're going to use is called **discriminated unions** (or "DUs" for short). You've probably dealt 33 | with `enum`s in C#; that is the closest parallel to DUs, but there are significant differences. Like `enum`s, DUs are 34 | an exhaustive list of all expected/valid values. Unlike `enum`s, though, they are not wrappers over another type; they 35 | are their own type. Also, each condition does not have to have the same type; it's perfectly valid to have a DU with 36 | one condition that has one type (or no type at all), and other condition with a completely different type. (We don't 37 | use that with these types.) 38 | 39 | To start, bring the `Entities.fs` file over from Tres, and ensure `Quatro.fsproj` has the JSON.Net dependency. Then, 40 | some code will help all this DU stuff make sense. One of the forms of a DU is called a single-case discriminated union; 41 | it can be used to wrap primitives to make them more meaningful. We'll create the following single-case DUs at the top 42 | of the file, before our other types: 43 | *) 44 | type CategoryId = CategoryId of string 45 | type CommentId = CommentId of string 46 | type PageId = PageId of string 47 | type PostId = PostId of string 48 | type UserId = UserId of string 49 | type WebLogId = WebLogId of string 50 | 51 | type Permalink = Permalink of string 52 | type Tag = Tag of string 53 | type Ticks = Ticks of int64 54 | type TimeZone = TimeZone of string 55 | type Url = Url of string 56 | (** 57 | It may be confusing that we're using the same name twice; the name after the `type` keyword defines the type, while the 58 | one after the equals sign defines the constructor for this type (`CategoryId "abc"` defines a category Id whose value 59 | is the string "abc"). We'll look at these implemented in a bit; next, though, we'll convert our 60 | static-classes-turned-modules into multi-case DUs. 61 | *) 62 | type AuthorizationLevel = 63 | | Administrator 64 | | User 65 | 66 | type PostStatus = 67 | | Draft 68 | | Published 69 | 70 | type CommentStatus = 71 | | Approved 72 | | Pending 73 | | Spam 74 | (** 75 | This is similar in concept to the single-case DUs, but there are no parameters required for the constructor. 76 | 77 | What does a record look like updated with these types? Let's revisit the `Page` type we dissected for Tres. 78 | *) 79 | type Page = { 80 | [] 81 | Id : PageId 82 | WebLogId : WebLogId 83 | AuthorId : UserId 84 | Title : string 85 | Permalink : Permalink 86 | PublishedOn : Ticks 87 | UpdatedOn : Ticks 88 | ShowInPageList : bool 89 | Text : string 90 | Revisions : Revision list 91 | } 92 | with 93 | static member Empty = 94 | { Id = PageId "" 95 | WebLogId = WebLogId "" 96 | AuthorId = UserId "" 97 | Title = "" 98 | Permalink = Permalink "" 99 | PublishedOn = Ticks 0L 100 | UpdatedOn = Ticks 0L 101 | ShowInPageList = false 102 | Text = "" 103 | Revisions = [] 104 | } 105 | (** 106 | The only primitives\* we now have are the `Title` and `Text` fields (which are both free-form text) and the 107 | `ShowInPageList` field (for which yes/no is sufficient, although we could create a `PageListVisibility` DU to constrain 108 | the yes/no values and distinguish them from others). The compiler will prevent us from crossing boundaries on every 109 | other field in this type! 110 | 111 | Let's take a look at the `Empty` property on the `Post` type to see a multi-case DU in use. 112 | *) 113 | (*** hide ***) 114 | type Post = { 115 | [] 116 | Id : PostId 117 | WebLogId : WebLogId 118 | AuthorId : UserId 119 | Status : PostStatus 120 | Title : string 121 | Permalink : string 122 | PublishedOn : Ticks 123 | UpdatedOn : Ticks 124 | Text : string 125 | CategoryIds : CategoryId list 126 | Tags : Tag list 127 | Revisions : Revision list 128 | } 129 | with 130 | (** *) 131 | static member Empty = 132 | { Id = PostId "new" 133 | WebLogId = WebLogId "" 134 | AuthorId = UserId "" 135 | Status = Draft 136 | Title = "" 137 | Permalink = "" 138 | PublishedOn = Ticks 0L 139 | UpdatedOn = Ticks 0L 140 | Text = "" 141 | CategoryIds = [] 142 | Tags = [] 143 | Revisions = [] 144 | } 145 | (** 146 | `Status` is defined as type `PostStatus`; to set its value, we simply have to write `Draft`. No quotes, no dotted 147 | access\*\*, just `Status = Draft`. (`Status = Spam` does not compile.) 148 | 149 | You can 150 | [review the entire set of types](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-2-core2/src/4-Freya-FSharp/Entities.fs) 151 | to see where these various DUs were used. While we could certainly take this much further, these simple changes have 152 | made our types more meaningful, while eliminating a lot of the invalid states we could have assigned in our code. 153 | 154 | [Back to Step 2](../step2) 155 | 156 | --- 157 | 158 | \* - `string` is a primitive for our purposes here. 159 | 160 | \*\* - If our DU condition is not unique, it would need to be qualified. For example, if we were to add a "Draft" 161 | `CommentStatus` so we could auto-save comment text while the visitor was typing\*\*\*, we would need to change the 162 | `Empty` property to assign `PostStatus.Draft` instead. Again, though, the compiler would help us spot that right away. 163 | 164 | \*\*\* - This is a really bad idea; don't do this. 165 | *) -------------------------------------------------------------------------------- /dox/content/step2/tres.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll" 3 | 4 | namespace Tres.Entities 5 | 6 | type Revision = { 7 | AsOf : int64 8 | Text : string 9 | } 10 | with 11 | static member Empty = 12 | { AsOf = 0L 13 | Text = "" 14 | } 15 | 16 | (** 17 | ### Tres - Step 2 18 | 19 | As we make the leap to F#, we're changing things around significantly. Remember our 20 | [discussion about the flat structure of an F# project](../step1/tres.html)? Instead of an `Entities` directory with a 21 | lot of little files, we'll define a single `Entities.fs` file in the root of the project. Don't forget to add it to 22 | the list of compiled files in `Tres.fsproj`; it should go above `HomeModule.fs`. Also, while we're in there, we'll add 23 | the JSON.Net dependency in the `ItemGroup` for dependencies: 24 | 25 | [lang=text] 26 | 27 | 28 | Next up, we will change the static classes that we created to eliminate magic strings into modules. The 29 | `AuthorizationLevel` type in C# looked like: 30 | 31 | [lang=csharp] 32 | public static class AuthorizationLevel 33 | { 34 | const string Administrator = "Administrator"; 35 | 36 | const string User = "User"; 37 | } 38 | 39 | The F# version (within the namespace `Tres.Entities`): 40 | *) 41 | [] 42 | module AuthorizationLevel = 43 | [] 44 | let Administrator = "Administrator" 45 | [] 46 | let User = "User" 47 | (** 48 | The `RequireQualifiedAccess` attribute means that this module cannot be `open`ed, which means that `Administrator` 49 | cannot ever be construed to be that value; it must be referenced as `AuthorizationLevel.Administrator`. The 50 | `Literal` attribute means that these values can be used in places where a literal string is required. (There is a 51 | specific place this will help us when we start writing code around these types.) Also of note here is the different 52 | way F# defines attributes from the way C# does; instead of `[` `]` pairs, we use `[<` `>]` pairs. 53 | 54 | We are also going to change from class types to record types. Record types can be thought of as `struct`s, though the 55 | comparison is not exact; record types are reference types, not value types, but they cannot be set to null **in code** 56 | _(huge caveat which we'll see in the next step)_ unless explicitly identified. We're also going to embrace F#'s 57 | immutability-by-default qualities that will save us a heap of null checks (as well as those pesky situations where we 58 | forget to implement them). 59 | 60 | As a representative example, consider the `Page` type. In C#, it looks like this: 61 | 62 | [lang=csharp] 63 | namespace Uno.Entities 64 | { 65 | using Newtonsoft.Json; 66 | using System.Collections.Generic; 67 | 68 | public class Page 69 | { 70 | [JsonProperty("id")] 71 | public string Id { get; set; } 72 | 73 | public string WebLogId { get; set; } 74 | 75 | public string AuthorId { get; set; } 76 | 77 | public string Title { get; set; } 78 | 79 | public string Permalink { get; set; } 80 | 81 | public long PublishedOn { get; set; } 82 | 83 | public long UpdatedOn { get; set; } 84 | 85 | public bool ShowInPageList { get; set; } 86 | 87 | public string Text { get; set; } 88 | 89 | public ICollection Revisions { get; set; } = new List(); 90 | } 91 | } 92 | 93 | It contains strings, for the most part, and a `Revisions` collection. Now, here's how we'll implement this same thing 94 | in F#: 95 | *) 96 | namespace Tres.Entities 97 | 98 | open Newtonsoft.Json 99 | //... 100 | type Page = { 101 | [] 102 | Id : string 103 | WebLogId : string 104 | AuthorId : string 105 | Title : string 106 | Permalink : string 107 | PublishedOn : int64 108 | UpdatedOn : int64 109 | ShowInPageList : bool 110 | Text : string 111 | Revisions : Revision list 112 | } 113 | with 114 | static member Empty = 115 | { Id = "" 116 | WebLogId = "" 117 | AuthorId = "" 118 | Title = "" 119 | Permalink = "" 120 | PublishedOn = 0L 121 | UpdatedOn = 0L 122 | ShowInPageList = false 123 | Text = "" 124 | Revisions = [] 125 | } 126 | (** 127 | The field declarations immediately under the `type` declaration mirror those in our C# version; since they are fields, 128 | though, we don't have to define getters and setters. 129 | 130 | F# requires record types to always have all fields defined. F# also provides a `with` statement (separate from the one 131 | in the code above) that allows us to create a new instance of a record type that has all the fields of our original 132 | ones, only replacing the ones we specify. So, in C#, while we can do something like 133 | 134 | [lang=csharp] 135 | var pg = new Page { Title = "Untitled" }; 136 | 137 | , leaving all the other fields in their otherwise-initialized state, F# will not allow us to do that. This is where 138 | the `Empty` static property comes in; we can use this to create new pages, while ensuring that we have sensible 139 | defaults for all the other fields. The equivalent to the above C# statement in F# would be 140 | *) 141 | let pg = { Page.Empty with Title = "Untitled" } 142 | (** 143 | . Note the default values for `Permalink`: in C#, it's null, but in F#, it's an empty string. Now, certainly, you can 144 | use `String.IsNullOrEmpty()` to check for both of those, but we'll see some advantages to this lack of nullability as 145 | we continue to develop this project. 146 | 147 | A few syntax notes: 148 | - `[]` represents an empty list in F#. An F# list (as distinguished from `System.Collections.List` or 149 | `System.Collections.Generic.List`) is also an immutable data structure; it consists of a head element, and a tail 150 | list. It can be constructed by creating a new list with an element as its head and the existing list as its tail, and 151 | deconstructed by processing the head, then processing the head of the tail, etc. (There are operators and functions to 152 | support that; we'll definitely use those as we go along.) Items in a list are separated by semicolons; 153 | `[ "one"; "two"; "three" ]` represents a `string list` with three items. It supports most all the collection 154 | operations you would expect, but there are some differences. 155 | - While not demonstrated here, arrays are defined between `[|` `|]` pairs, also with elements separated by semicolons. 156 | 157 | Before continuing on to [Quatro](quatro.html), you should familiarize yourself with the 158 | [types in this step](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-2-core2/src/3-Nancy-FSharp/Entities.fs). 159 | 160 | [Back to Step 2](../step2) 161 | *) -------------------------------------------------------------------------------- /dox/content/step2/uno-dos.md: -------------------------------------------------------------------------------- 1 | ### Uno/Dos - Step 2 2 | 3 | As the overview page mentioned, this model is pretty straightforward. There are three static classes 4 | (`AuthorizationLevel`, `CommentStatus`, and `PostStatus`) to keep us from using magic strings in the code. The objects 5 | that will actually be stored in the database are `Category`, `Comment`, `Page`, `Post`, `User`, and `WebLog`. 6 | `Revision` and `Authorization` give structure to the collections stored within other objects (`Page`/`Post` and 7 | `User`, respectively). 8 | 9 | To support the `JsonProperty` attribute, we'll need to add a reference to JSON.Net; this goes in the `.csproj` file, in 10 | the `ItemGroup` with dependencies: 11 | 12 | [lang=text] 13 | 14 | 15 | If you're reading through this for learning, you'll want to familiarize yourself with the 16 | [files as they are in C# at this step](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-2-core2/src/1-AspNetCore-CSharp/Entities) 17 | before continuing to [Tres](tres.html) and [Quatro](quatro.html). 18 | 19 | [Back to Step 2](../step2) -------------------------------------------------------------------------------- /dox/content/step3/dos.md: -------------------------------------------------------------------------------- 1 | ### Dos - Step 3 2 | 3 | This step will follow the same general path as [Uno](./uno.html), but we'll also iterate on what we made there to bring 4 | some of the code into a more functional style. 5 | 6 | #### `Dos.csproj` Changes 7 | 8 | We only need to add the RethinkDB package to what we had at the end of step 2 (in the dependency `ItemGroup`): 9 | 10 | [lang=text] 11 | 12 | 13 | Run `dotnet restore` to pull in that dependency. Also, we'll bring across the entire `Data` directory we created in 14 | **Uno** during this step. We'll be able to use `Table.cs` and `EnvironmentExtensions.cs` as is (except for changing 15 | the namespace to `Dos.Data`). 16 | 17 | #### Configurable Connection 18 | 19 | Since `appsettings.json` is a .NET Core thing, we will not use it here. We can still use JSON to configure our 20 | connection, though; here's the `data-config.json` file: 21 | 22 | [lang=text] 23 | { 24 | "Hostname": "my-rethinkdb-server", 25 | "Database": "O2F2" 26 | } 27 | 28 | To support this, we'll need to change a couple of other things. First, in our `DataConfig` file, we'll add the 29 | following using statement and static method: 30 | 31 | [lang=csharp] 32 | using Newtonsoft.Json; 33 | ... 34 | public static DataConfig FromJson(string json) => JsonConvert.DeserializeObject(json); 35 | 36 | Now we can create our configuration from this JSON once we read it in. We're also going to modify the 37 | `CreateConnection()` method; we'll also add some more `using`s and a support property. 38 | 39 | [lang=csharp] 40 | using System; 41 | using System.Collections.Generic; 42 | using System.Linq; 43 | using static RethinkDb.Driver.Net.Connection; 44 | ... 45 | private IEnumerable> Blocks 46 | { 47 | get 48 | { 49 | yield return builder => null == Hostname ? builder : builder.Hostname(Hostname); 50 | yield return builder => 0 == Port ? builder : builder.Port(Port); 51 | yield return builder => null == AuthKey ? builder : builder.AuthKey(AuthKey); 52 | yield return builder => null == Database ? builder : builder.Db(Database); 53 | yield return builder => 0 == Timeout ? builder : builder.Timeout(Timeout); 54 | } 55 | } 56 | 57 | public IConnection CreateConnection() => 58 | Blocks.Aggregate(RethinkDB.R.Connection(), (builder, block) => block(builder)).Connect(); 59 | 60 | If this is the first time you've seen code like this, you may be thinking "Why would we do something like that?" We're 61 | moving from an imperative style, like we used in **Uno**, to a more declarative style. `Blocks` is an enumeration (or 62 | sequence) where each item yielded takes a connection builder, and returns either the original connection builder it 63 | received or one that has been modified with a configuration parameter. If you didn't understand that last sentence, 64 | look at the code, then read it again; understanding this structure will pay dividends once we're knee-deep in F#. 65 | 66 | Once you understand the `Blocks` property, take a look at the `CreateConnection()` method. This uses the LINQ method 67 | `Aggregate`, which takes two parameters: an initial state; and a function that will be given the current state and each 68 | item, and will return the "current" state after that item has been processed. If that makes no sense, imagine you had 69 | a sequence `exSeq` with the letters "A", "B", and "C". If you were to run 70 | `var str = exSeq.Aggregate("", (state, letter) => String.Format("{0},{1}", state, letter));`, `str` would hold the 71 | string ",A,B,C". The "initial state" is simply the starting value; but, every iteration must return a value of that 72 | same type. 73 | 74 | Hopefully `Aggregate` is making sense at this point. Taking a forward-looking side trip - we're going to see it, with 75 | a different parameter order, as the `fold` function in our F# code. You've likely heard the term "map/reduce" - this 76 | describes a process where, given a data collection, you can transform it into a shape you need (map) and distill that 77 | data into the answer you need (reduce). (Yes, purists, this is a bit of a simplification.) F# provides `map` and 78 | `reduce` implementations for several collection types; however, `reduce` cannot produce a type different from that of 79 | the underlying collection - `fold` is what does that. 80 | 81 | Back from our side trip, what this code does is: 82 | 83 | - Seeds `Aggregate` with `RethinkDB.R.Connection()`, which is an instance of `Connection.Builder` _(`Builder` is a 84 | nested type within `RethinkDb.Driver.Net.Connection`; the `using static` makes it visible the way we've used it here.)_ 85 | - Loops through each item of our enumeration. Since each item is a `Func`, we pass the item the 86 | current builder; it returns a builder that may have been further configured ("aggregating" our configuration). 87 | - Once the `Aggregate` has completed, we're ready to call `Connect()` on our connection builder, and return that from 88 | our method. 89 | 90 | Seeing a more functional style with C# should help when we start seeing F# collections. 91 | 92 | #### Dependency Injection 93 | 94 | With Nancy, if you want to add forks to the SDHP, you have to provide a bootstrapper that will handle the startup code. 95 | For most purposes, the best way is to simply override `DefaultNancyBootstrapper`; that way, any code you don't provide 96 | will use the default, and you can call `base` methods from your overridden ones, so all the SDHP magic continues to 97 | work. 98 | 99 | Here's the custom bootstrapper we'll use: 100 | 101 | [lang=csharp] 102 | namespace Dos 103 | { 104 | using Dos.Data; 105 | using Nancy; 106 | using Nancy.TinyIoc; 107 | using System.IO; 108 | 109 | public class DosBootstrapper : DefaultNancyBootstrapper 110 | { 111 | public DosBootstrapper() : base() { } 112 | 113 | protected override void ConfigureApplicationContainer(TinyIoCContainer container) 114 | { 115 | base.ConfigureApplicationContainer(container); 116 | 117 | var cfg = DataConfig.FromJson(File.ReadAllText("data-config.json")); 118 | var conn = cfg.CreateConnection(); 119 | conn.EstablishEnvironment(cfg.Database).GetAwaiter().GetResult(); 120 | container.Register(conn); 121 | } 122 | } 123 | } 124 | 125 | This looks very similar to the code from the ASP.NET Core implementation, with the exception of how we're getting the 126 | configuration. We're all done except for two minor fixes. First, we need to tell Nancy to use this bootstrapper 127 | instead of the default. This is `Startup.cs`: 128 | 129 | [lang=csharp] 130 | public void Configure(IApplicationBuilder app) => 131 | app.UseOwin(x => x.UseNancy(options => options.Bootstrapper = new DosBootstrapper())); 132 | 133 | Finally, we need to specify that our `data-config.json` file should be copied to the output directory; otherwise, it 134 | will just sit on the hard drive while you scratch your head trying to figure out why your application can't connect. 135 | _(Ask me how I know...)_ This change is in `project.json`, just under the `emitEntryPoint` declaration (included here 136 | for context): 137 | 138 | [lang=text] 139 | "emitEntryPoint": true, 140 | "copyToOutput": { 141 | "include": "data-config.json" 142 | } 143 | 144 | At this point, you should be able to `dotnet run` and, once the console says that it's listening, you should be able to 145 | see the database, tables, and indexes in the `O2F2` database. 146 | 147 | [Back to Step 3](../step3) -------------------------------------------------------------------------------- /dox/content/step3/index.md: -------------------------------------------------------------------------------- 1 | ## RethinkDB Connection 2 | 3 | Database connections are generally defined at either the application or request instance levels. The 4 | [C# RethinkDB driver](https://github.com/bchavez/RethinkDb.Driver) is designed for the former, and configuring it as a 5 | singleton is the recommended implementation. We will do that for our application. In the process of ensuring that we 6 | can properly configure this instance, we will also have to address the concepts of configuration and dependency 7 | injection (or, in the case of our Freya implementation, its replacement). You can review the all the code at 8 | [the checkpoint for step 3](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-3-core2). 9 | 10 | ### A Bit about RethinkDB 11 | 12 | RethinkDB is a document database, and the C# implementation allows us to represent our documents as Plain Old CLR 13 | Objects (AKA POCOs). It uses JSON.Net to serialize POCOs into JSON documents, which are then stored by the server. 14 | It exposes collections of documents as "databases" and "tables" within each database, mirroring its relational database 15 | cousins. However, there are no schemas for tables, and a table can have documents of varying formats. Each table can 16 | have one or more indexes that can be used to retrieve documents without scanning the entire table. It provides its own 17 | query language (ReQL) that utilizes a fluent interface, where queries begin with an `R.` or `r.` and end in a `Run*` 18 | statement. 19 | 20 | It has other features, such as server clustering and change feeds, but these will not be part of our project (although 21 | change feeds could be an interesting Step n-2 project for post comments). We will use a single instance of RethinkDB, 22 | and a single database within it, for all our data. 23 | 24 | ### All Implementations 25 | 26 | Each of our implementations will allow for the following user-configurable options: 27 | 28 | - `Hostname` - The hostname for the server; defaults to "localhost" 29 | - `Port` - The port to use to connect to the server; defaults to 28015 30 | - `Database` - The default database to use for queries that do not specify a database; defaults to "test" 31 | - `AuthKey` - The authorization key to provide for the connection; defaults to "" (empty) 32 | - `Timeout` - How long to wait when connecting; defaults to 20 33 | 34 | Additionally, we will write start-up code that ensures our requisite tables exist, and that each of these tables has 35 | the appropriate indexes. To hold our documents, we will create the following tables: 36 | 37 | - Category 38 | - Comment 39 | - Page 40 | - Post 41 | - User 42 | - WebLog 43 | 44 | We will be able to retrieve individual documents by Id without any further definition. Additionally, we will create 45 | indexes to support the following scenarios: 46 | 47 | - Category retrieval by web log Id _(used to generate lists of available categories)_ 48 | - Category retrieval by web log Id and slug _(used to retrieve details for category archive pages)_ 49 | - Comment retrieval by post Id _(used to retrieve comments for a post)_ 50 | - Page retrieval by web log Id _(used to retrieve a list of pages)_ 51 | - Page retrieval by permalink _(used to fulfill single page requests)_ 52 | - Post retrieval by web log Id _(used many places)_ 53 | - Post retrieval by category Id _(used to get posts for category archive pages)_ 54 | - Post retrieval by tag _(used to get posts for tag archive pages)_ 55 | - User retrieval by e-mail address and password _(used for log in)_ 56 | 57 | ### Individual Implementations 58 | 59 | Each of these not only evolves from step 2 to step 3, they also evolve as Uno moves to Quatro. It may help 60 | understanding to read each of them, even if your interest is just in one of them. 61 | 62 | **Uno** - [In Depth](uno.html) 63 | 64 | **Dos** - [In Depth](dos.html) 65 | 66 | **Tres** - [In Depth](tres.html) 67 | 68 | **Quatro** - [In Depth](quatro.html) -------------------------------------------------------------------------------- /dox/content/step3/quatro.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Chiron/lib/net40/Chiron.dll" 3 | #r "../../../packages/Freya.Core/lib/net452/Freya.Core.dll" 4 | #r "../../../packages/Freya.Machines.Http/lib/net452/Freya.Machines.Http.dll" 5 | #r "../../../packages/Freya.Routers.Uri.Template/lib/net452/Freya.Routers.Uri.Template.dll" 6 | #r "../../../packages/Freya.Types.Uri.Template/lib/net452/Freya.Types.Uri.Template.dll" 7 | #r "../../../packages/Microsoft.AspNetCore.Hosting/lib/net451/Microsoft.AspNetCore.Hosting.dll" 8 | #r "../../../packages/Microsoft.AspNetCore.Hosting.Abstractions/lib/net451/Microsoft.AspNetCore.Hosting.Abstractions.dll" 9 | #r "../../../packages/Microsoft.AspNetCore.Http.Abstractions/lib/net451/Microsoft.AspNetCore.Http.Abstractions.dll" 10 | #r "../../../packages/Microsoft.AspNetCore.Owin/lib/net451/Microsoft.AspNetCore.Owin.dll" 11 | #r "../../../packages/Microsoft.AspNetCore.Server.Kestrel/lib/net451/Microsoft.AspNetCore.Server.Kestrel.dll" 12 | #r "../../../packages/RethinkDb.Driver/lib/net45/RethinkDb.Driver.dll" 13 | 14 | (** 15 | ### Quatro - Step 3 16 | 17 | As with our previous versions, we'll start by adding the RethinkDB driver to `project.json`; we'll also bring the 18 | `data-config.json` file from **Dos**/**Tres** into this project, changing the database name to `O2F4`. Follow the 19 | [instructions for Tres](tres.html) up though the point where it says "we'll create a file `Data.fs`". 20 | 21 | **Parsing `data.config`** 22 | 23 | We'll use `Data.fs` in this project as well, but we'll do things a bit more functionally. We'll use 24 | [Chiron](https://xyncro.tech/chiron/) to parse the JSON file, and we'll set up a discriminated union (DU) for our 25 | configuration parameters. 26 | 27 | First, to be able to use Chiron, we'll need the package. Add the following line within the `dependencies` section: 28 | 29 | [lang=text] 30 | "Chiron": "6.2.1", 31 | 32 | Then, we'll start `Data.fs` with our DU. 33 | *) 34 | namespace Quatro 35 | 36 | open Chiron 37 | // other opens 38 | type ConfigParameter = 39 | | Hostname of string 40 | | Port of int 41 | | AuthKey of string 42 | | Timeout of int 43 | | Database of string 44 | (*** hide ***) 45 | open RethinkDb.Driver 46 | open RethinkDb.Driver.Net 47 | open System 48 | open System.IO 49 | 50 | // -- begin code lifted from #er demo -- 51 | (*** define: readerm-definition ***) 52 | type ReaderM<'d, 'out> = 'd -> 'out 53 | (*** hide ***) 54 | module Reader = 55 | (** *) 56 | (*** define: run ***) 57 | let run dep (rm : ReaderM<_,_>) = rm dep 58 | (** *) 59 | (*** define: lift-dep ***) 60 | let liftDep (proj : 'd2 -> 'd1) (rm : ReaderM<'d1, 'output>) : ReaderM<'d2, 'output> = proj >> rm 61 | (** *) 62 | (*** hide ***) 63 | open Reader 64 | (** 65 | This DU looks a bit different than the single-case DUs or enum-style DUs that 66 | [we made in step 2](../step2/quatro.html). This is a full-fledged DU with 5 different types, 3 strings and 2 integers. 67 | The `DataConfig` record now becomes dead simple: 68 | *) 69 | type DataConfig = { Parameters : ConfigParameter list } 70 | (** 71 | We'll populate that using Chiron's `Json.parse` function. 72 | *) 73 | with 74 | static member FromJson json = 75 | match Json.parse json with 76 | | Object config -> 77 | let options = 78 | config 79 | |> Map.toList 80 | |> List.map (fun item -> 81 | match item with 82 | | "Hostname", String x -> Hostname x 83 | | "Port", Number x -> Port <| int x 84 | | "AuthKey", String x -> AuthKey x 85 | | "Timeout", Number x -> Timeout <| int x 86 | | "Database", String x -> Database x 87 | | key, value -> 88 | raise <| InvalidOperationException 89 | (sprintf "Unrecognized RethinkDB configuration parameter %s (value %A)" key value)) 90 | { Parameters = options } 91 | | _ -> { Parameters = [] } 92 | (*** define: database-property ***) 93 | member this.Database = 94 | match this.Parameters 95 | |> List.filter (fun x -> match x with Database _ -> true | _ -> false) 96 | |> List.tryHead with 97 | | Some (Database x) -> x 98 | | _ -> RethinkDBConstants.DefaultDbName 99 | 100 | (** 101 | There is a lot to learn in these lines. 102 | 103 | * Before, if the JSON didn't parse, we raised an exception, but that was about it. In this one, if the JSON doesn't 104 | parse, we get a default connection. Maybe this is better, maybe not, but it demonstrates that there is a way to handle 105 | bad JSON other than an exception. 106 | * `Object`, `String`, and `Number` are Chiron types (cases of a DU, actually), so our `match` statement uses the 107 | destructuring form to "unwrap" the DU's inner value. For `String`, `x` is a string, and for `Number`, `x` is a decimal 108 | (that's why we run it through `int` to make our DUs. 109 | * This version will raise an exception if we attempt to set an option that we do not recognize (something like 110 | "databsae" - not that anyone I know would ever type it like that...). 111 | 112 | Now, we'll adapt the `CreateConnection ()` function to read this new configuration representation: 113 | *) 114 | member this.CreateConnection () : IConnection = 115 | let folder (builder : Connection.Builder) block = 116 | match block with 117 | | Hostname x -> builder.Hostname x 118 | | Port x -> builder.Port x 119 | | AuthKey x -> builder.AuthKey x 120 | | Timeout x -> builder.Timeout x 121 | | Database x -> builder.Db x 122 | let bldr = 123 | this.Parameters 124 | |> Seq.fold folder (RethinkDB.R.Connection ()) 125 | upcast bldr.Connect() 126 | (** 127 | Our folder function utilizes a `match` on our `ConfigParameter` DU. Each time through, it **will** return a modified 128 | version of the `builder` parameter, because one of them will match. We then create our builder by folding the 129 | parameter, using `R.Connection ()` as our beginning state, then return its `Connect ()` method. 130 | 131 | For now, let's copy the rest of `Data.fs` from **Tres** to **Quatro** - this gives us the table constants and the 132 | table/index initialization code. 133 | 134 | **Dependency Injection: Functional Style** 135 | 136 | One of the concepts that dependency injection is said to implement is "inversion of control;" rather than an object 137 | compiling and linking a dependency at compile time, it compiles against an interface, and the concrete implementation 138 | is provided at runtime. (This is a bit of an oversimplification, but it's the basic gist.) If you've ever done 139 | non-DI/non-IoC work, and learned DI, you've adjusted your thinking from "what do I need" to "what will I need". In the 140 | functional world, this is done through a concept called the **`Reader` monad**. The basic concept is as follows: 141 | 142 | * We have a set of dependencies that we establish and set up in our code. 143 | * We a process with a dependency that we want to be injected (in our example, our `IConnection` is one such 144 | dependency). 145 | * We construct a function that requires this dependency, and returns the result we seek. Though we won't see it in 146 | this step, it's easy to imagine a function that requires an `IConnection` and returns a `Post`. 147 | * We create a function that, given our set of dependencies, will extract the one we need for this process. 148 | * We run our dependencies through the extraction function, to the dependent function, which takes the dependency and 149 | returns the result. 150 | 151 | Confused yet? Me too - let's look at code instead. Let's create `Dependencies.fs` and add it to the build order above 152 | `Entities.fs`. This write-up won't expound on every line in this file, but we'll hit the highlights to see how all 153 | this comes together. `ReaderM` is a generic class, where the first type is the dependency we need, and the second type 154 | is the type of our result. 155 | 156 | After that (which will come back to in a bit), we'll create our dependencies, and a function to extract an 157 | `IConnection` from it. 158 | *) 159 | type IDependencies = 160 | abstract Conn : IConnection 161 | 162 | [] 163 | module DependencyExtraction = 164 | 165 | let getConn (deps : IDependencies) = deps.Conn 166 | (*** hide ***) 167 | [] 168 | module ExampleExtensions = 169 | open System.Threading.Tasks 170 | 171 | // H/T: Suave 172 | type AsyncBuilder with 173 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 174 | /// a standard .NET task 175 | member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 176 | 177 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 178 | /// a standard .NET task which does not commpute a value 179 | member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 180 | 181 | member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t 182 | (** *) 183 | (*** define: data-module ***) 184 | [] 185 | module Data = 186 | let establishEnvironment database conn = 187 | let r = RethinkDB.R 188 | // etc. 189 | (*** hide ***) 190 | let checkDatabase (db : string) = async { return () } 191 | let checkTables () = async { return () } 192 | let checkIndexes () = 193 | let indexesFor tbl = async { return! r.Table(tbl).IndexList().RunResultAsync conn } 194 | async { return () } 195 | async { 196 | do! checkDatabase database 197 | do! checkTables () 198 | do! checkIndexes () 199 | } 200 | 201 | (** 202 | Our `IDependencies` are pretty lean right now, but that's OK; we'll flesh it out in future steps. We also wrote a 203 | dead-easy function to get the connection; the signature is literally `IDependencies -> IConnection`. No `ReaderM` 204 | funkiness yet! 205 | 206 | Now that we have a dependency "set" (of one), we need to go to `App.fs` and make sure we actually have a concrete 207 | instance of this for runtime. Add this just below the module declaration: 208 | *) 209 | (*** hide ***) 210 | [] 211 | module IConnectionExtensions = 212 | type IConnection with 213 | member this.EstablishEnvironment (database : string) = async { return () } 214 | module App = 215 | (** *) 216 | let lazyCfg = lazy (File.ReadAllText "data-config.json" |> DataConfig.FromJson) 217 | let cfg = lazyCfg.Force() 218 | let deps = { 219 | new IDependencies with 220 | member __.Conn 221 | with get () = 222 | let conn = lazy (cfg.CreateConnection ()) 223 | conn.Force() 224 | } 225 | (** 226 | Here, we're using `lazy` to do this once-only-and-only-on-demand, then we turn around and pretty much demand it. If 227 | you're thinking this sounds a lot like singletons - your thinking is superb! That's exactly what we're doing here. 228 | We're also using F#'s inline interface declaration to create an implementation without creating a concrete class in 229 | which it is held. 230 | 231 | Maybe being our own IoC container isn't so bad! Now, let's take a stab at actually connection, and running the 232 | `EstablishEnvironment` function on startup. At the top of `main`: 233 | *) 234 | (*** hide ***) 235 | let main _ = 236 | (** *) 237 | let initDb (conn : IConnection) = conn.EstablishEnvironment cfg.Database |> Async.RunSynchronously 238 | let start = liftDep getConn initDb 239 | start |> run deps 240 | (*** define: better-init ***) 241 | liftDep getConn (Data.establishEnvironment cfg.Database >> Async.RunSynchronously) 242 | |> run deps 243 | (*** define: composition-almost ***) 244 | let almost = Data.establishEnvironment cfg.Database 245 | (*** define: composition-money ***) 246 | let money = Data.establishEnvironment cfg.Database >> Async.RunSynchronously 247 | (*** hide ***) 248 | 0 249 | (** 250 | But wait - we don't have a `Database` property on our data config; our configuration is just a list of 251 | `ConfigParameter` selections. No worries, though; we can expose a database property on it pretty easily. 252 | *) 253 | 254 | (*** include: database-property ***) 255 | 256 | (** 257 | OK - now our red squiggly lines are gone. Now, if Jiminy Cricket had written F#, he would have told Pinocchio "Let the 258 | types be your guide". So, how are we doing with these? `initDb` has the signature `IConnection -> unit`, `start` has 259 | the signature `ReaderM`, and the third line is simply `unit`. And, were we to run it, it would 260 | work, but... it's not really composable. 261 | 262 | Creating extension methods on objects works great in C#-land, and as we've seen, it works the same way in F#-land. 263 | However, in the case where we want to write functions that expect an `IConnection` and return our expected result, 264 | extension methods are not what we need. Let's change our `AutoOpen`ed `DataExtensions` module to something like this: 265 | *) 266 | (*** include: data-module ***) 267 | (** 268 | Now, we have a function with the signature `string -> IConnection -> Async`. This gets us close, but we still 269 | have issues on either side of that signature. On the front, if we were just hard-coding the database name, we could 270 | drop the string parameter, and we'd have our `IConnection` as the first parameter. On the return value, we will need 271 | to run the `Async` workflow (remember, in F#, they're not started automatically); we need `unit`, not `Async`. 272 | 273 | We'll use two key F# concepts to fix this up. Currying (also known as partial application) allows us to look at every 274 | return value that isn't the result as a function that's one step closer. Looking at our signature above, you could 275 | express it in English as "a function that takes a string, and returns a function that takes an `IConnection` and 276 | returns an `Async` workflow." So, to get a function that starts with an `IConnection`, we just provide the database 277 | name. 278 | *) 279 | (*** include: composition-almost ***) 280 | (** 281 | The signature for `almost` is `IConnection -> Async`. Just what we want. For the latter, we use composition. 282 | This is a word that can be used, for example, to describe the way the collection modules expect the collection as the 283 | final parameter, allowing the output of one to be piped, using the `|>` operator, to the input of the next. The other 284 | is with the `>>` operator, which says to use the output of the first function as the input of the second function, 285 | creating a single function from the two. This is the one we'll use to run our `Async` workflow. 286 | *) 287 | (*** include: composition-money ***) 288 | (** 289 | The signature for `money` is now `IConnection -> unit`, just like we need. 290 | 291 | Now, let's revisit `initDb` above. Since we don't need the `IConnection` as a parameter, we can change that definition 292 | to the same thing we have for `money` above. And, since we don't need the parameter, we can just inline the call after 293 | `getConn`; we'll just need to wrap the expression in parentheses to indicate that it's a function on its own. And, we 294 | don't need the definition of `start` anymore either - we can just pipe our entire expression into `run deps`. 295 | *) 296 | (*** include: better-init ***) 297 | (** 298 | It works! We set up our dependencies, we composed a function using a dependency, and we used a `Reader` monad to make 299 | it all work. But, how did it work? Given what we just learned above, let's look at the steps; we're coders, not 300 | magicians. 301 | 302 | First up, `liftDeps`. 303 | *) 304 | (*** include: lift-dep ***) 305 | (** 306 | The `proj` parameter is defined as a function that takes one value and returns another one. The `rm` parameter is a 307 | `Reader` monad that takes the **return** value of `proj`, and returns a `Reader` monad that takes the **parameter** 308 | value of `proj` and returns an output type. We passed `getConn` as the `proj` parameter, and its signature is 309 | `IDependencies -> IConnection`; the second parameter was a function with the signature `IConnection -> unit`. Where 310 | does this turn into a `ReaderM`? Why, the definition, of course! 311 | *) 312 | (*** include: readerm-definition ***) 313 | (** 314 | So, `liftDep` derived the expected `ReaderM` type from `getConn`; `'d1` is `IDependencies` and `'d2` is `IConnection`. 315 | This means that the next parameter should be a function which takes an `IConnection` and returns the output of the type 316 | we expect. Since we pass in `IConnection -> unit`, `output` is `unit`. When all is said and done, if we were to 317 | assign a value to the top line, we would end up with `ReaderM`. 318 | 319 | Now, to run it. `run` is defined as: 320 | *) 321 | (*** include: run ***) 322 | (** 323 | This is way easier than what we've seen up to this point. It takes an object and a `ReaderM`, and applies the object 324 | to the first parameter of the monad. By `|>`ing the `ReaderM` to it, and providing our 325 | `IDependencies` instance, we receive the result; the reader has successfully encapsulated all the functions below it. 326 | From this point on, we'll just make sure our types are correct, and we'll be able to utilize not only an `IConnection` 327 | for data manipulation, but any other dependencies we may need to define. 328 | 329 | Take a deep breath. Step 3 is done, and not only does it work, we understand why it works. 330 | 331 | [Back to Step 3](../step3) 332 | *) -------------------------------------------------------------------------------- /dox/content/step3/tres.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | #r "../../../packages/Microsoft.AspNetCore.Http.Abstractions/lib/net451/Microsoft.AspNetCore.Http.Abstractions.dll" 3 | #r "../../../packages/Microsoft.AspNetCore.Owin/lib/net451/Microsoft.AspNetCore.Owin.dll" 4 | #r "../../../packages/Nancy/lib/net452/Nancy.dll" 5 | #r "../../../packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll" 6 | #r "../../../packages/RethinkDb.Driver/lib/net45/RethinkDb.Driver.dll" 7 | 8 | (** 9 | ### Tres - Step 3 10 | 11 | We'll start out, as we did with **Dos**, with adding the RethinkDB dependency to `project.json`'s `dependencies` 12 | section: 13 | 14 | [lang=text] 15 | "RethinkDb.Driver": "2.3.15" 16 | 17 | `dotnet restore` installs it as usual. 18 | 19 | #### Configurable Connection 20 | 21 | Since **Tres** is more-or-less a C#-to-F# conversion from **Dos**, we'll use the same `data-config.json` file, in the 22 | root of the project: 23 | 24 | [lang=text] 25 | { 26 | "Hostname": "my-rethinkdb-server", 27 | "Database": "O2F3" 28 | } 29 | 30 | We'll also add it to `project.json`, just below the list of files to compile: 31 | 32 | [lang=text] 33 | "copyToOutput": { 34 | "include": "data-config.json" 35 | } 36 | 37 | Now, we'll create a file `Data.fs` to hold what we had in the `Data` directory in the prior two solutions. It should 38 | be added to the build order after `Entities.fs` and before `HomeModule.fs`. We'll start this file off with our 39 | `DataConfig` implementation: 40 | *) 41 | namespace Tres 42 | 43 | open Newtonsoft.Json 44 | open RethinkDb.Driver 45 | open RethinkDb.Driver.Ast 46 | open RethinkDb.Driver.Net 47 | 48 | type DataConfig = 49 | { Hostname : string 50 | Port : int 51 | AuthKey : string 52 | Timeout : int 53 | Database : string 54 | } 55 | with 56 | member this.CreateConnection () : IConnection = 57 | let bldr = 58 | seq Connection.Builder> { 59 | yield fun builder -> match this.Hostname with null -> builder | host -> builder.Hostname host 60 | yield fun builder -> match this.Port with 0 -> builder | port -> builder.Port port 61 | yield fun builder -> match this.AuthKey with null -> builder | key -> builder.AuthKey key 62 | yield fun builder -> match this.Database with null -> builder | db -> builder.Db db 63 | yield fun builder -> match this.Timeout with 0 -> builder | timeout -> builder.Timeout timeout 64 | } 65 | |> Seq.fold (fun builder block -> block builder) (RethinkDB.R.Connection()) 66 | upcast bldr.Connect() 67 | static member FromJson json = JsonConvert.DeserializeObject json 68 | (** 69 | This should be familiar at this point; we're using a record type instead of a class, and the `CreateConnection` 70 | function utilizes the sequence style from **Dos**, just inlined as a _computation expression_ (more on those in a bit). 71 | We also see `Seq.fold`, which takes the parameters in pretty much the opposite order of LINQ's `Aggregate`; instead of 72 | `[collection].Aggregate([initial-state], [folder-func])`, it's `Seq.fold [folder-func] [initial-state] [collection]` 73 | (which we're piping in with the `|>` operator). 74 | 75 | The `upcast` is new. Notice that `CreateConnection` is typed as `IConnection`; what's returned from the connection 76 | builder is a `Connection`. In most cases, F# requires an implementation to be explicitly cast to the interface it is 77 | claiming to implement. In our case, we want `IConnection` (vs. `IDisposable`, which it also implements). There are 78 | two ways to do this; if the type can be inferred, as it can be here (because we've explicitly said what our return type 79 | should be), you can use `upcast`. Alternately, the last line of that function could read 80 | `(bldr.Connect()) :> IConnection`. _(I tend to prefer `upcast` when possible.)_ 81 | 82 | At this point, we need to take a detour through the land of asynchronous processing. **Uno** and **Dos** both used 83 | async/await to perform the RethinkDB calls, utilizing the `Task`-based async introduced in .NET 4.5. F#'s approach to 84 | asynchrony is different, but there are a few functions that provide the interoperability we need. F# also uses an 85 | `async` _computation expression_ to construct these. The most important difference, for our purposes here, is that F# 86 | `Async` instances are not automatically started the way a `Task` is in the C# world. 87 | 88 | And, a quick detour from the detour - I promised there would be more on computation expressions. These are expressions 89 | that utilize an expression builder to declaratively create workflows within code. They typically operate in a 90 | specialized context; we've seen `seq`, we're about to see `async`, but there are many other uses as well. Within a 91 | computation expression, `let` and `do` have their same familiar behavior, and `return` returns a value; however, 92 | `let!`, `do!`, and `return!` call into the builder to manipulate the specialized context. A complete education in 93 | computation expressions is outside of our scope; _F# for Fun and Profit_ has an 94 | [excellent series on them](http://fsharpforfunandprofit.com/series/computation-expressions.html). 95 | 96 | Before we see our first `async` computation expression, though, we need to make it be able to handle `Task`s as well as 97 | F# async. The code for `Extensions.fs` is below. I won't delve too far into it at this point, but trust that what it 98 | does is let us say `let! x = someTask` and it works just like it was F# async. (This code is in the `AutoOpen`ed 99 | module `Tres.Extensions`.) 100 | *) 101 | (*** hide ***) 102 | [] 103 | module ExampleExtensions = 104 | (** *) 105 | open System.Threading.Tasks 106 | 107 | // H/T: Suave 108 | type AsyncBuilder with 109 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 110 | /// a standard .NET task 111 | member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 112 | 113 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 114 | /// a standard .NET task which does not commpute a value 115 | member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 116 | 117 | member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t 118 | 119 | (** 120 | Add `Extensions.fs` to the build order at the top. With those in place, we are now ready to return to `Data.fs` and 121 | our startup code. Before we move on, review 122 | [the startup code from Dos](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-3/src/2-Nancy-CSharp/Data/EnvironmentExtensions.cs); 123 | it has a main driver method at the top, with several methods below to perform each step. Our F# code structure will be 124 | somewhat inverted from this; as you generally cannot call forward into a source file, the "driver" code will be the 125 | last code in the function, and the other code above it. 126 | 127 | The `Table.cs` static class with constants is brought over as a module (below the data configuration code). The 128 | `RequireQualifiedAccess` attribute means that `Table` cannot be `open`ed; this prevents us from possibly providing an 129 | unintended version of the identifier `Post` (for example). 130 | *) 131 | [] 132 | module Table = 133 | let Category = "Category" 134 | let Comment = "Comment" 135 | let Page = "Page" 136 | let Post = "Post" 137 | let User = "User" 138 | let WebLog = "WebLog" 139 | (** 140 | Extensions are defined differently in F#. Whereas C# uses a static class and methods where the first parameter has 141 | `this` before it, F# uses a module for the definitions (usually `AutoOpen`ed, so they're visible when the enclosing 142 | namespace is opened), and a `type` declaration. Here's the top of ours (below the table module): 143 | *) 144 | [] 145 | module DataExtensions = 146 | type IConnection with 147 | member this.EstablishEnvironment database = 148 | // more to come... 149 | (*** hide ***) 150 | let r = RethinkDB.R 151 | (** 152 | Rather than go through the entire file, let's just look at a representative example. Here is the code to check for 153 | table existence in C#: 154 | 155 | [lang=csharp] 156 | private static async Task CheckTables(this IConnection conn) 157 | { 158 | var existing = await R.TableList().RunResultAsync>(conn); 159 | var tables = new List 160 | { 161 | Table.Category, Table.Comment, Table.Page, Table.Post, Table.User, Table.WebLog 162 | }; 163 | foreach (var table in tables) 164 | { 165 | if (!existing.Contains(table)) { await R.TableCreate(table).RunResultAsync(conn); } 166 | } 167 | } 168 | 169 | Now, here's what it looks like in F#: 170 | *) 171 | let checkTables () = 172 | async { 173 | let! existing = r.TableList().RunResultAsync this 174 | [ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ] 175 | |> List.filter (fun tbl -> not (existing |> List.contains tbl)) 176 | |> List.map (fun tbl -> async { do! r.TableCreate(tbl).RunResultAsync this }) 177 | |> List.iter Async.RunSynchronously 178 | } 179 | (*** hide ***) 180 | let checkDatabase (db : string) = async { return () } 181 | let checkIndexes () = async { return () } 182 | (** 183 | The more interesting differences: 184 | 185 | - In C#, `existing` is awaited; in F#, we use `let!` within the `async` computation expression to accomplish the same 186 | thing. 187 | - In C#, we defined a `List` that we filled with our table names; in F#, we inlined a `string list`. 188 | - In C#, we have a nice imperative loop that iterates over each table, checks to see whether it is in the list of 189 | tables from the server, and creates it if it is not. In F#, we declare that the list should be filtered to only names 190 | not occurring in the list (`List.filter`); then that each of those names should be turned into an `Async` that will 191 | create the table when it's run (`List.map`); then, that the list should be iterated, passing each item into 192 | `Async.RunSynchronously` (`List.iter`). 193 | - In C#, the return type of the method is `Task`; in F#, the type of `checkTables` is `Async`. 194 | - When the C# `CheckTables` method call returns, the work has already been done. When the F# `checkTables` function 195 | returns, it returns an async workflow that is ready to be started. 196 | 197 | The last 5 lines of the `EstablishEnvironment` extension method look like this: 198 | *) 199 | async { 200 | do! checkDatabase database 201 | do! checkTables () 202 | do! checkIndexes () 203 | } 204 | (*** hide ***) 205 | open Microsoft.AspNetCore.Builder 206 | open Nancy 207 | open Nancy.Owin 208 | open System.IO 209 | (** 210 | There are a few interesting observations here as well: 211 | 212 | - `do!` is the equivalent of `let!`, except that we don't care about the result. 213 | - As we saw above, `checkTables` returns an async workflow; yet, we're `do!`ing it in yet another async workflow; this 214 | is perfectly acceptable. If you've ever added async/await to a C# application, usually at a lower layer, and noticed 215 | how async and await bubble up to the top layer - that's a similar concept to what we have here. 216 | - `EstablishEnvironment`'s return type is `Async`. It **still** hasn't run anything at this point; it has merely 217 | assembled an asynchronous workflow that will do all of our environment checks once it is run. 218 | 219 | #### Dependency Injection 220 | 221 | We'll do the same thing we did for **Dos** - override `DefaultNancyBootstrapper` and register our connection there. 222 | We'll do all of this in `App.fs`. The first part, above the definition for `type Startup()`: 223 | *) 224 | type TresBootstrapper() = 225 | inherit DefaultNancyBootstrapper() 226 | 227 | override this.ConfigureApplicationContainer container = 228 | base.ConfigureApplicationContainer container 229 | let cfg = DataConfig.FromJson (File.ReadAllText "data-config.json") 230 | let conn = cfg.CreateConnection () 231 | conn.EstablishEnvironment cfg.Database |> Async.RunSynchronously 232 | container.Register conn |> ignore 233 | (** 234 | Ah ha! **There's** where we finally run our async workflow! Now, again, we need to modify `Startup` (just below where 235 | we put this code) to use this new bootstrapper. 236 | *) 237 | member this.Configure (app : IApplicationBuilder) = 238 | app.UseOwin (fun x -> x.UseNancy (fun opt -> opt.Bootstrapper <- new TresBootstrapper()) |> ignore) |> ignore 239 | (** 240 | At this point, once `dotnet run` displays the "listening on port 5000" message, we should be able to look at 241 | RethinkDB's `O2F3` database, tables, and indexes, just as we could for **Uno** and **Dos**. 242 | 243 | [Back to Step 3](../step3) 244 | *) -------------------------------------------------------------------------------- /dox/content/step3/uno.md: -------------------------------------------------------------------------------- 1 | ### Uno - Step 3 2 | 3 | Our implementation here will fall into two broad categories - defining the configurable connection and table/index 4 | checking code that we can run at startup, and configuring ASP.NET Core's DI container to wire it all up. Before we get 5 | to that, though, we need to add a few packages to `Uno.csproj` (under the dependency `ItemGroup`) for this step. 6 | 7 | [lang=text] 8 | 9 | 10 | 11 | 12 | 13 | #### Configurable Connection 14 | 15 | Our application will need an instance of RethinkDB's `IConnection` to utilize. To support our configuration options, 16 | we will make a POCO called `DataConfig`, under a new `Data` directory in our project, and also give it an instance 17 | method to create the connection with the current values. 18 | 19 | [lang=csharp] 20 | namespace Uno.Data 21 | { 22 | using RethinkDb.Driver; 23 | using RethinkDb.Driver.Net; 24 | 25 | public class DataConfig 26 | { 27 | public string Hostname { get; set; } 28 | 29 | public int Port { get; set; } 30 | 31 | public string AuthKey { get; set; } 32 | 33 | public int Timeout { get; set; } 34 | 35 | public string Database { get; set; } 36 | 37 | public IConnection CreateConnection() 38 | { 39 | var conn = RethinkDB.R.Connection(); 40 | 41 | if (null != Hostname) { conn = conn.Hostname(Hostname); } 42 | if (0 != Port) { conn = conn.Port(Port); } 43 | if (null != AuthKey) { conn = conn.AuthKey(AuthKey); } 44 | if (null != Database) { conn = conn.Db(Database); } 45 | if (0 != Timeout) { conn = conn.Timeout(Timeout); } 46 | 47 | return conn.Connect(); 48 | } 49 | } 50 | } 51 | 52 | Note that the connection builder uses a fluent interface. We just as well could have chained all of these together, 53 | using defaults where we had no data, like so: 54 | 55 | [lang=csharp] 56 | RethinkDB.R.Connection() 57 | .Hostname(Hostname ?? RethinkDBConstants.DefaultHostname) 58 | .Port(0 == Port ? RethinkDBConstants.DefaultPort : Port) 59 | ...etc... 60 | .Connect(); 61 | 62 | We could then actually define this as a fat-arrow (`=>`) function and omit the return. If C# were our final 63 | destination, that's a fine implementation; of course, it's not, and I've structured it this way to illustrate that we 64 | really only have to call the configuration methods for properties that we've specified in our JSON file. 65 | 66 | Note also that we are mutating the `conn` variable with the result of each builder call. Do we need to do this? I 67 | have no idea; if the C# driver is (under the hood) mutating itself, we don't; if it's returning a new version of the 68 | builder with a change made (the F#/immutable way of doing things), we do. I certainly could find out _(yay, open 69 | source!)_, but it's an implementation detail we don't need to know. It's not wrong to do it this way, and in future 70 | implementations, we will be accomplishing the same thing without using mutation - at least in our code. 71 | 72 | #### Tables 73 | 74 | RethinkDB uses the term "table" to represent a collection of documents. Other document databases use the term 75 | "collection" or "document store"; this is the rough equivalent of a relational table. Of course, the difference here 76 | is that the documents do not all have to conform to the same schema. `Data/Table.cs` contains C# constants we will use 77 | to reference our tables. 78 | 79 | #### Ensuring Tables and Indexes Exist 80 | 81 | Many of the new APIs that are provided within .NET Core are implemented as extension methods on existing objects. 82 | Since `IConnection` represents our connection to RethinkDB, we'll target that type for our extension methods. We 83 | create the `EnvironmentExtensions.cs` file under the `Data` directory, and define it as a `public static` class. 84 | 85 | In our overall plan for step 3, we defined several types of queries we want to be able to run against these tables. 86 | While RethinkDB will create a table the first time you try to store a document in it, we cannot define indexes against 87 | them in this scenario. Indexes are the way RethinkDB avoids a complete table scan for documents; the concept is very 88 | similar to an index on a relational table. Since we need to define these indexes before our application can use them, 89 | we'll need make sure the tables exist, so we can create indexes against them. 90 | 91 | We will not go line-by-line through `EnvironmentExtensions.cs`; it's rather straightforward, and simply ensures that 92 | the database, tables, and indexes exist. It is our first exposure to the RethinkDB API, though, so be sure to 93 | [review the source](https://github.com/danieljsummers/FromObjectsToFunctions/tree/step-3-core2/src/1-AspNetCore-CSharp/Data/EnvironmentExtensions.cs) 94 | to ensure you get a sense of how data access is designed to work in the RethinkDB driver. 95 | 96 | #### Dependency Injection 97 | 98 | Now that we have defined our connection, and a method to make sure we have the data environment we need, we need a 99 | connection. `appsettings.json` is the standard .NET Core name for the configuration file, so we create one with the 100 | following values: 101 | 102 | [lang=text] 103 | { 104 | "RethinkDB": { 105 | "Hostname": "my-rethinkdb-server", 106 | "Database": "O2F1" 107 | } 108 | } 109 | 110 | The database name, here `O2F1`, will be different in each of our examples; this way, we can verify that each of our 111 | instances created the tables and indexes correctly. 112 | 113 | When we were doing our quick-and-dirty "Hello World" in step 1, we had very minimal content in `Startup.cs`. Now, 114 | we'll flesh that out a little more. 115 | 116 | [lang=csharp] 117 | [add] 118 | using Microsoft.AspNetCore.Hosting; 119 | using Microsoft.Extensions.Configuration; 120 | using Microsoft.Extensions.DependencyInjection; 121 | using Microsoft.Extensions.Options; 122 | using Uno.Data; 123 | [/add] 124 | 125 | public class Startup 126 | { 127 | public static IConfigurationRoot Configuration { get; private set; } 128 | 129 | public Startup(IHostingEnvironment env) 130 | { 131 | var builder = new ConfigurationBuilder() 132 | .SetBasePath(env.ContentRootPath) 133 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 134 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 135 | .AddEnvironmentVariables(); 136 | Configuration = builder.Build(); 137 | } 138 | 139 | public void ConfigureServices(IServiceCollection services) 140 | { 141 | services.AddOptions(); 142 | services.Configure(Configuration.GetSection("RethinkDB")); 143 | 144 | var cfg = services.BuildServiceProvider().GetService>().Value; 145 | var conn = cfg.CreateConnection(); 146 | conn.EstablishEnvironment(cfg.Database).GetAwaiter().GetResult(); 147 | services.AddSingleton(conn); 148 | } 149 | 150 | This does the following: 151 | 152 | - Creates a configuration tree that is a union of `appsettings.json`, `appsettings.{environment}.json`, and environment 153 | variables (each of those overriding the prior one if settings are specified in both) 154 | - Establishes the new `Options` API, registers our `DataConfig` as an option set, and specifies that it should be 155 | obtained from the `RethinkDB` section of the configuration 156 | - Creates a connection based on our configuration 157 | - Runs the `EstablishEnvironment` extension method, so that when we're done, we have the tables and indexes we expect 158 | _(since it's an `async` method, we use the `.GetAwaiter().GetResult()` chain so we don't have to define 159 | `ConfigureServices` as `async`)_ 160 | - Registers our `IConnection` for injection 161 | 162 | Now, if we build and run our application, then use RethinkDB's administration site to look at our server, we should now 163 | see an `O2F1` database created, along with our tables and indexes. 164 | 165 | [Back to Step 3](../step3) -------------------------------------------------------------------------------- /dox/files/img/logo-template.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljsummers/FromObjectsToFunctions/0b67eb49b727ed12a8c1016bfa11c59b717dc8e3/dox/files/img/logo-template.pdn -------------------------------------------------------------------------------- /dox/files/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljsummers/FromObjectsToFunctions/0b67eb49b727ed12a8c1016bfa11c59b717dc8e3/dox/files/img/logo.png -------------------------------------------------------------------------------- /dox/tools/generate.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // Builds the documentation from `.fsx` and `.md` files in the 'docs/content' directory 3 | // (the generated documentation is stored in the 'docs/output' directory) 4 | // -------------------------------------------------------------------------------------- 5 | 6 | // Binaries that have XML documentation (in a corresponding generated XML file) 7 | // Any binary output / copied to bin/projectName/projectName.dll will 8 | // automatically be added as a binary to generate API docs for. 9 | // for binaries output to root bin folder please add the filename only to the 10 | // referenceBinaries list below in order to generate documentation for the binaries. 11 | // (This is the original behaviour of ProjectScaffold prior to multi project support) 12 | let referenceBinaries = [] 13 | // Web site location for the generated documentation 14 | let website = "/FromObjectsToFunctions" 15 | 16 | let githubLink = "https://github.com/danieljsummers/FromObjectsToFunctions" 17 | 18 | // Specify more information about your project 19 | let info = 20 | [ "project-name", "objects () |> functions" 21 | "project-author", "Daniel J. Summers" 22 | "project-summary", "An attempt to provide a concrete, working example to demonstrate to C# developers how F# can improve their workflow and performance" 23 | "project-github", githubLink 24 | "project-nuget", "http://nuget.org/packages/FromObjectsToFunctions" ] 25 | 26 | // -------------------------------------------------------------------------------------- 27 | // For typical project, no changes are needed below 28 | // -------------------------------------------------------------------------------------- 29 | 30 | #load "../../packages/FSharp.Formatting/FSharp.Formatting.fsx" 31 | #I "../../packages/FAKE/tools/" 32 | #r "FakeLib.dll" 33 | open Fake 34 | open System.IO 35 | open Fake.FileHelper 36 | open FSharp.Literate 37 | open FSharp.MetadataFormat 38 | 39 | // When called from 'build.fsx', use the public project URL as 40 | // otherwise, use the current 'output' directory. 41 | #if RELEASE 42 | let root = website 43 | #else 44 | let root = "file://" + (__SOURCE_DIRECTORY__ @@ "../output") 45 | #endif 46 | 47 | // Paths with template/source/output locations 48 | let bin = __SOURCE_DIRECTORY__ @@ "../../bin" 49 | let content = __SOURCE_DIRECTORY__ @@ "../content" 50 | let output = __SOURCE_DIRECTORY__ @@ "../output" 51 | let files = __SOURCE_DIRECTORY__ @@ "../files" 52 | let templates = __SOURCE_DIRECTORY__ @@ "templates" 53 | let formatting = __SOURCE_DIRECTORY__ @@ "../../packages/FSharp.Formatting/" 54 | let docTemplate = "docpage.cshtml" 55 | 56 | // Where to look for *.csproj templates (in this order) 57 | let layoutRootsAll = new System.Collections.Generic.Dictionary() 58 | layoutRootsAll.Add("en",[ templates; formatting @@ "templates" 59 | formatting @@ "templates/reference" ]) 60 | subDirectories (directoryInfo templates) 61 | |> Seq.iter (fun d -> 62 | let name = d.Name 63 | if name.Length = 2 || name.Length = 3 then 64 | layoutRootsAll.Add( 65 | name, [templates @@ name 66 | formatting @@ "templates" 67 | formatting @@ "templates/reference" ])) 68 | 69 | // Copy static files and CSS + JS from F# Formatting 70 | let copyFiles () = 71 | CopyRecursive files output true |> Log "Copying file: " 72 | ensureDirectory (output @@ "content") 73 | CopyRecursive (formatting @@ "styles") (output @@ "content") true 74 | |> Log "Copying styles and scripts: " 75 | 76 | let binaries = 77 | let manuallyAdded = 78 | referenceBinaries 79 | |> List.map (fun b -> bin @@ b) 80 | 81 | let conventionBased = [] 82 | (*directoryInfo bin 83 | |> subDirectories 84 | |> Array.map (fun d -> d.FullName @@ (sprintf "%s.dll" d.Name)) 85 | |> List.ofArray *) 86 | 87 | conventionBased @ manuallyAdded 88 | 89 | let libDirs = [] 90 | (*let conventionBasedbinDirs = 91 | directoryInfo bin 92 | |> subDirectories 93 | |> Array.map (fun d -> d.FullName) 94 | |> List.ofArray 95 | 96 | conventionBasedbinDirs @ [bin]*) 97 | 98 | // Build API reference from XML comments 99 | let buildReference () = 100 | CleanDir (output @@ "reference") 101 | MetadataFormat.Generate 102 | ( binaries, output @@ "reference", layoutRootsAll.["en"], 103 | parameters = ("root", root)::info, 104 | sourceRepo = githubLink @@ "tree/master", 105 | sourceFolder = __SOURCE_DIRECTORY__ @@ ".." @@ "..", 106 | publicOnly = true,libDirs = libDirs ) 107 | 108 | // Build documentation from `fsx` and `md` files in `docs/content` 109 | let buildDocumentation () = 110 | 111 | // First, process files which are placed in the content root directory. 112 | 113 | Literate.ProcessDirectory 114 | ( content, docTemplate, output, replacements = ("root", root)::info, 115 | layoutRoots = layoutRootsAll.["en"], 116 | generateAnchors = true, 117 | processRecursive = false) 118 | 119 | // And then process files which are placed in the sub directories 120 | // (some sub directories might be for specific language). 121 | 122 | let subdirs = Directory.EnumerateDirectories(content, "*", SearchOption.TopDirectoryOnly) 123 | for dir in subdirs do 124 | let dirname = (new DirectoryInfo(dir)).Name 125 | let layoutRoots = 126 | // Check whether this directory name is for specific language 127 | let key = layoutRootsAll.Keys 128 | |> Seq.tryFind (fun i -> i = dirname) 129 | match key with 130 | | Some lang -> layoutRootsAll.[lang] 131 | | None -> layoutRootsAll.["en"] // "en" is the default language 132 | 133 | Literate.ProcessDirectory 134 | ( dir, docTemplate, output @@ dirname, replacements = ("root", root)::info, 135 | layoutRoots = layoutRoots, 136 | generateAnchors = true ) 137 | 138 | // Generate 139 | copyFiles() 140 | //#if HELP 141 | buildDocumentation() 142 | //#endif 143 | //#if REFERENCE 144 | //buildReference() 145 | //#endif 146 | -------------------------------------------------------------------------------- /dox/tools/templates/template.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | @Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 | 29 |

@Properties["project-name"]

30 |
31 |
32 |
33 |
34 | @RenderBody() 35 |
36 |
37 | F# Project 38 | 55 |
56 |
57 |
58 | Fork me on GitHub 59 | 60 | 61 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://nuget.org/api/v2 2 | 3 | nuget FSharp.Core redirects: force 4 | 5 | nuget Chiron 6 | nuget FAKE 7 | nuget Freya >= 3.0.0-rc01 8 | nuget FSharp.Formatting 9 | nuget Microsoft.AspNetCore.Owin 10 | nuget Microsoft.AspNetCore.Server.Kestrel 11 | nuget Nancy >= 2.0-barneyrubble 12 | nuget RethinkDb.Driver -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/App.cs: -------------------------------------------------------------------------------- 1 | namespace Uno 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | public class App 6 | { 7 | public static void Main(string[] args) 8 | { 9 | using (var host = new WebHostBuilder().UseKestrel().UseStartup().Build()) 10 | { 11 | host.Run(); 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Data/DataConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Data 2 | { 3 | using RethinkDb.Driver; 4 | using RethinkDb.Driver.Net; 5 | 6 | public class DataConfig 7 | { 8 | public string Hostname { get; set; } 9 | 10 | public int Port { get; set; } 11 | 12 | public string AuthKey { get; set; } 13 | 14 | public int Timeout { get; set; } 15 | 16 | public string Database { get; set; } 17 | 18 | public IConnection CreateConnection() 19 | { 20 | var conn = RethinkDB.R.Connection(); 21 | 22 | if (null != Hostname) { conn = conn.Hostname(Hostname); } 23 | if (0 != Port) { conn = conn.Port(Port); } 24 | if (null != AuthKey) { conn = conn.AuthKey(AuthKey); } 25 | if (null != Database) { conn = conn.Db(Database); } 26 | if (0 != Timeout) { conn = conn.Timeout(Timeout); } 27 | 28 | return conn.Connect(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Data/EnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Data 2 | { 3 | using RethinkDb.Driver; 4 | using RethinkDb.Driver.Net; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using static RethinkDb.Driver.RethinkDB; 8 | 9 | public static class EnvironmentExtensions 10 | { 11 | public static async Task EstablishEnvironment(this IConnection conn, string database) 12 | { 13 | await conn.CheckDatabase(database); 14 | await conn.CheckTables(); 15 | await conn.CheckIndexes(); 16 | } 17 | 18 | private static async Task CheckDatabase(this IConnection conn, string database) 19 | { 20 | if (!string.IsNullOrEmpty(database) && !R.DbList().RunResult>(conn).Contains(database)) 21 | { 22 | await R.DbCreate(database).RunResultAsync(conn); 23 | } 24 | } 25 | 26 | private static async Task CheckTables(this IConnection conn) 27 | { 28 | var existing = await R.TableList().RunResultAsync>(conn); 29 | var tables = new List 30 | { 31 | Table.Category, Table.Comment, Table.Page, Table.Post, Table.User, Table.WebLog 32 | }; 33 | foreach (var table in tables) 34 | { 35 | if (!existing.Contains(table)) { await R.TableCreate(table).RunResultAsync(conn); } 36 | } 37 | } 38 | 39 | private static async Task CheckIndexes(this IConnection conn) 40 | { 41 | await conn.CheckCategoryIndexes(); 42 | await conn.CheckCommentIndexes(); 43 | await conn.CheckPageIndexes(); 44 | await conn.CheckPostIndexes(); 45 | await conn.CheckUserIndexes(); 46 | } 47 | 48 | private static async Task CheckCategoryIndexes(this IConnection conn) 49 | { 50 | var indexes = await conn.IndexesFor(Table.Category); 51 | if (!indexes.Contains("WebLogId")) 52 | { 53 | await R.Table(Table.Category).IndexCreate("WebLogId").RunResultAsync(conn); 54 | } 55 | if (!indexes.Contains("WebLogAndSlug")) 56 | { 57 | await R.Table(Table.Category) 58 | .IndexCreate("WebLogAndSlug", row => R.Array(row["WebLogId"], row["Slug"])) 59 | .RunResultAsync(conn); 60 | } 61 | } 62 | 63 | private static async Task CheckCommentIndexes(this IConnection conn) 64 | { 65 | if (!(await conn.IndexesFor(Table.Comment)).Contains("PostId")) 66 | { 67 | await R.Table(Table.Comment).IndexCreate("PostId").RunResultAsync(conn); 68 | } 69 | } 70 | 71 | private static async Task CheckPageIndexes(this IConnection conn) 72 | { 73 | var indexes = await conn.IndexesFor(Table.Page); 74 | if (!indexes.Contains("WebLogId")) 75 | { 76 | await R.Table(Table.Page).IndexCreate("WebLogId").RunResultAsync(conn); 77 | } 78 | if (!indexes.Contains("WebLogAndPermalink")) 79 | { 80 | await R.Table(Table.Page) 81 | .IndexCreate("WebLogAndPermalink", row => R.Array(row["WebLogId"], row["Permalink"])) 82 | .RunResultAsync(conn); 83 | } 84 | } 85 | 86 | private static async Task CheckPostIndexes(this IConnection conn) 87 | { 88 | var indexes = await conn.IndexesFor(Table.Post); 89 | if (!indexes.Contains("WebLogId")) 90 | { 91 | await R.Table(Table.Post).IndexCreate("WebLogId").RunResultAsync(conn); 92 | } 93 | if (!indexes.Contains("Tags")) 94 | { 95 | await R.Table(Table.Post).IndexCreate("Tags").OptArg("multi", true).RunResultAsync(conn); 96 | } 97 | } 98 | 99 | private static async Task CheckUserIndexes(this IConnection conn) 100 | { 101 | if (!(await conn.IndexesFor(Table.User)).Contains("EmailAddress")) 102 | { 103 | await R.Table(Table.User).IndexCreate("EmailAddress").RunResultAsync(conn); 104 | } 105 | } 106 | 107 | private static Task> IndexesFor(this IConnection conn, string table) => 108 | R.Table(table).IndexList().RunResultAsync>(conn); 109 | } 110 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Data/Table.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Data 2 | { 3 | public static class Table 4 | { 5 | public const string Category = "Category"; 6 | 7 | public const string Comment = "Comment"; 8 | 9 | public const string Page = "Page"; 10 | 11 | public const string Post = "Post"; 12 | 13 | public const string User = "User"; 14 | 15 | public const string WebLog = "WebLog"; 16 | } 17 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Authorization.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | public class Authorization 4 | { 5 | public string WebLogId { get; set; } 6 | 7 | public string Level { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/AuthorizationLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | public static class AuthorizationLevel 4 | { 5 | const string Administrator = "Administrator"; 6 | 7 | const string User = "User"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Category 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string Name { get; set; } 14 | 15 | public string Slug { get; set; } 16 | 17 | public string Description { get; set; } 18 | 19 | public string ParentId { get; set; } 20 | 21 | public ICollection Children { get; set; } = new List(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Comment.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | 5 | public class Comment 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | public string PostId { get; set; } 11 | 12 | public string InReplyToId { get; set; } 13 | 14 | public string Name { get; set; } 15 | 16 | public string EmailAddress { get; set; } 17 | 18 | public string Url { get; set; } 19 | 20 | public string Status { get; set; } 21 | 22 | public long PostedOn { get; set; } 23 | 24 | public string Text { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/CommentStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | public static class CommentStatus 4 | { 5 | const string Approved = "Approved"; 6 | 7 | const string Pending = "Pending"; 8 | 9 | const string Spam = "Spam"; 10 | } 11 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Page.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Page 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string AuthorId { get; set; } 14 | 15 | public string Title { get; set; } 16 | 17 | public string Permalink { get; set; } 18 | 19 | public long PublishedOn { get; set; } 20 | 21 | public long UpdatedOn { get; set; } 22 | 23 | public bool ShowInPageList { get; set; } 24 | 25 | public string Text { get; set; } 26 | 27 | public ICollection Revisions { get; set; } = new List(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Post.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Post 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string AuthorId { get; set; } 14 | 15 | public string Status { get; set; } 16 | 17 | public string Title { get; set; } 18 | 19 | public string Permalink { get; set; } 20 | 21 | public long PostedOn { get; set; } 22 | 23 | public long UpdatedOn { get; set; } 24 | 25 | public string Text { get; set; } 26 | 27 | public ICollection CategoryIds { get; set; } = new List(); 28 | 29 | public ICollection Tags { get; set; } = new List(); 30 | 31 | public ICollection Revisions { get; set; } = new List(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/PostStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | public static class PostStatus 4 | { 5 | const string Published = "Published"; 6 | 7 | const string Draft = "Draft"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/Revision.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | public class Revision 4 | { 5 | public long AsOf { get; set; } 6 | 7 | public string Text { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class User 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string EmailAddress { get; set; } 12 | 13 | public string PasswordHash { get; set; } 14 | 15 | public string FirstName { get; set; } 16 | 17 | public string LastName { get; set; } 18 | 19 | public string PreferredName { get; set; } 20 | 21 | public string Url { get; set; } 22 | 23 | public ICollection Authorizations { get; set; } = new List(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Entities/WebLog.cs: -------------------------------------------------------------------------------- 1 | namespace Uno.Entities 2 | { 3 | using Newtonsoft.Json; 4 | 5 | public class WebLog 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string Subtitle { get; set; } 13 | 14 | public string DefaultPage { get; set; } 15 | 16 | public string ThemePath { get; set; } 17 | 18 | public string UrlBase { get; set; } 19 | 20 | public string TimeZone { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace Uno 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Options; 9 | using System.Threading.Tasks; 10 | using Uno.Data; 11 | 12 | public class Startup 13 | { 14 | public static IConfigurationRoot Configuration { get; private set; } 15 | 16 | public Startup(IHostingEnvironment env) 17 | { 18 | System.Console.WriteLine("Content root = " + env.ContentRootPath); 19 | var builder = new ConfigurationBuilder() 20 | .SetBasePath(env.ContentRootPath) 21 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 22 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 23 | .AddEnvironmentVariables(); 24 | Configuration = builder.Build(); 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddOptions(); 30 | services.Configure(Configuration.GetSection("RethinkDB")); 31 | 32 | var cfg = services.BuildServiceProvider().GetService>().Value; 33 | var conn = cfg.CreateConnection(); 34 | conn.EstablishEnvironment(cfg.Database).GetAwaiter().GetResult(); 35 | services.AddSingleton(conn); 36 | } 37 | 38 | public void Configure(IApplicationBuilder app) => 39 | app.Run(async context => await context.Response.WriteAsync("Hello World from ASP.NET Core")); 40 | } 41 | } -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/Uno.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Uno 5 | 1.0.0 6 | Exe 7 | netcoreapp2.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/1-AspNetCore-CSharp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RethinkDB": { 3 | "Hostname": "severus-server", 4 | "Database": "O2F1" 5 | } 6 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/App.cs: -------------------------------------------------------------------------------- 1 | namespace Dos 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | public class App 6 | { 7 | public static void Main(string[] args) 8 | { 9 | using (var host = new WebHostBuilder().UseKestrel().UseStartup().Build()) 10 | { 11 | host.Run(); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Data/DataConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Data 2 | { 3 | using Newtonsoft.Json; 4 | using RethinkDb.Driver; 5 | using RethinkDb.Driver.Net; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using static RethinkDb.Driver.Net.Connection; 10 | 11 | public class DataConfig 12 | { 13 | public string Hostname { get; set; } 14 | 15 | public int Port { get; set; } 16 | 17 | public string AuthKey { get; set; } 18 | 19 | public int Timeout { get; set; } 20 | 21 | public string Database { get; set; } 22 | 23 | private IEnumerable> Blocks 24 | { 25 | get 26 | { 27 | yield return builder => null == Hostname ? builder : builder.Hostname(Hostname); 28 | yield return builder => 0 == Port ? builder : builder.Port(Port); 29 | yield return builder => null == AuthKey ? builder : builder.AuthKey(AuthKey); 30 | yield return builder => null == Database ? builder : builder.Db(Database); 31 | yield return builder => 0 == Timeout ? builder : builder.Timeout(Timeout); 32 | } 33 | } 34 | 35 | public IConnection CreateConnection() => 36 | Blocks.Aggregate(RethinkDB.R.Connection(), (builder, block) => block(builder)).Connect(); 37 | 38 | public static DataConfig FromJson(string json) => JsonConvert.DeserializeObject(json); 39 | } 40 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Data/EnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Data 2 | { 3 | using RethinkDb.Driver; 4 | using RethinkDb.Driver.Net; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using static RethinkDb.Driver.RethinkDB; 8 | 9 | public static class EnvironmentExtensions 10 | { 11 | public static async Task EstablishEnvironment(this IConnection conn, string database) 12 | { 13 | await conn.CheckDatabase(database); 14 | await conn.CheckTables(); 15 | await conn.CheckIndexes(); 16 | } 17 | 18 | private static async Task CheckDatabase(this IConnection conn, string database) 19 | { 20 | if (!string.IsNullOrEmpty(database) && !R.DbList().RunResult>(conn).Contains(database)) 21 | { 22 | await R.DbCreate(database).RunResultAsync(conn); 23 | } 24 | } 25 | 26 | private static async Task CheckTables(this IConnection conn) 27 | { 28 | var existing = await R.TableList().RunResultAsync>(conn); 29 | var tables = new List 30 | { 31 | Table.Category, Table.Comment, Table.Page, Table.Post, Table.User, Table.WebLog 32 | }; 33 | foreach (var table in tables) 34 | { 35 | if (!existing.Contains(table)) { await R.TableCreate(table).RunResultAsync(conn); } 36 | } 37 | } 38 | 39 | private static async Task CheckIndexes(this IConnection conn) 40 | { 41 | await conn.CheckCategoryIndexes(); 42 | await conn.CheckCommentIndexes(); 43 | await conn.CheckPageIndexes(); 44 | await conn.CheckPostIndexes(); 45 | await conn.CheckUserIndexes(); 46 | } 47 | 48 | private static async Task CheckCategoryIndexes(this IConnection conn) 49 | { 50 | var indexes = await conn.IndexesFor(Table.Category); 51 | if (!indexes.Contains("WebLogId")) 52 | { 53 | await R.Table(Table.Category).IndexCreate("WebLogId").RunResultAsync(conn); 54 | } 55 | if (!indexes.Contains("WebLogAndSlug")) 56 | { 57 | await R.Table(Table.Category) 58 | .IndexCreate("WebLogAndSlug", row => R.Array(row["WebLogId"], row["Slug"])) 59 | .RunResultAsync(conn); 60 | } 61 | } 62 | 63 | private static async Task CheckCommentIndexes(this IConnection conn) 64 | { 65 | if (!(await conn.IndexesFor(Table.Comment)).Contains("PostId")) 66 | { 67 | await R.Table(Table.Comment).IndexCreate("PostId").RunResultAsync(conn); 68 | } 69 | } 70 | 71 | private static async Task CheckPageIndexes(this IConnection conn) 72 | { 73 | var indexes = await conn.IndexesFor(Table.Page); 74 | if (!indexes.Contains("WebLogId")) 75 | { 76 | await R.Table(Table.Page).IndexCreate("WebLogId").RunResultAsync(conn); 77 | } 78 | if (!indexes.Contains("WebLogAndPermalink")) 79 | { 80 | await R.Table(Table.Page) 81 | .IndexCreate("WebLogAndPermalink", row => R.Array(row["WebLogId"], row["Permalink"])) 82 | .RunResultAsync(conn); 83 | } 84 | } 85 | 86 | private static async Task CheckPostIndexes(this IConnection conn) 87 | { 88 | var indexes = await conn.IndexesFor(Table.Post); 89 | if (!indexes.Contains("WebLogId")) 90 | { 91 | await R.Table(Table.Post).IndexCreate("WebLogId").RunResultAsync(conn); 92 | } 93 | if (!indexes.Contains("Tags")) 94 | { 95 | await R.Table(Table.Post).IndexCreate("Tags").OptArg("multi", true).RunResultAsync(conn); 96 | } 97 | } 98 | 99 | private static async Task CheckUserIndexes(this IConnection conn) 100 | { 101 | if (!(await conn.IndexesFor(Table.User)).Contains("EmailAddress")) 102 | { 103 | await R.Table(Table.User).IndexCreate("EmailAddress").RunResultAsync(conn); 104 | } 105 | } 106 | 107 | private static Task> IndexesFor(this IConnection conn, string table) => 108 | R.Table(table).IndexList().RunResultAsync>(conn); 109 | } 110 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Data/Table.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Data 2 | { 3 | public static class Table 4 | { 5 | public const string Category = "Category"; 6 | 7 | public const string Comment = "Comment"; 8 | 9 | public const string Page = "Page"; 10 | 11 | public const string Post = "Post"; 12 | 13 | public const string User = "User"; 14 | 15 | public const string WebLog = "WebLog"; 16 | } 17 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Dos.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dos 5 | 1.0.0 6 | Exe 7 | netcoreapp2.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/DosBootstrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Dos 2 | { 3 | using Dos.Data; 4 | using Nancy; 5 | using Nancy.TinyIoc; 6 | using System.IO; 7 | 8 | public class DosBootstrapper : DefaultNancyBootstrapper 9 | { 10 | public DosBootstrapper() : base() { } 11 | 12 | protected override void ConfigureApplicationContainer(TinyIoCContainer container) 13 | { 14 | base.ConfigureApplicationContainer(container); 15 | 16 | var cfg = DataConfig.FromJson(File.ReadAllText("data-config.json")); 17 | var conn = cfg.CreateConnection(); 18 | conn.EstablishEnvironment(cfg.Database).GetAwaiter().GetResult(); 19 | container.Register(conn); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Authorization.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | public class Authorization 4 | { 5 | public string WebLogId { get; set; } 6 | 7 | public string Level { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/AuthorizationLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | public static class AuthorizationLevel 4 | { 5 | const string Administrator = "Administrator"; 6 | 7 | const string User = "User"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Category 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string Name { get; set; } 14 | 15 | public string Slug { get; set; } 16 | 17 | public string Description { get; set; } 18 | 19 | public string ParentId { get; set; } 20 | 21 | public ICollection Children { get; set; } = new List(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Comment.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | 5 | public class Comment 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | public string PostId { get; set; } 11 | 12 | public string InReplyToId { get; set; } 13 | 14 | public string Name { get; set; } 15 | 16 | public string EmailAddress { get; set; } 17 | 18 | public string Url { get; set; } 19 | 20 | public string Status { get; set; } 21 | 22 | public long PostedOn { get; set; } 23 | 24 | public string Text { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/CommentStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | public static class CommentStatus 4 | { 5 | const string Approved = "Approved"; 6 | 7 | const string Pending = "Pending"; 8 | 9 | const string Spam = "Spam"; 10 | } 11 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Page.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Page 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string AuthorId { get; set; } 14 | 15 | public string Title { get; set; } 16 | 17 | public string Permalink { get; set; } 18 | 19 | public long PublishedOn { get; set; } 20 | 21 | public long UpdatedOn { get; set; } 22 | 23 | public bool ShowInPageList { get; set; } 24 | 25 | public string Text { get; set; } 26 | 27 | public ICollection Revisions { get; set; } = new List(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Post.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class Post 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string WebLogId { get; set; } 12 | 13 | public string AuthorId { get; set; } 14 | 15 | public string Status { get; set; } 16 | 17 | public string Title { get; set; } 18 | 19 | public string Permalink { get; set; } 20 | 21 | public long PostedOn { get; set; } 22 | 23 | public long UpdatedOn { get; set; } 24 | 25 | public string Text { get; set; } 26 | 27 | public ICollection CategoryIds { get; set; } = new List(); 28 | 29 | public ICollection Tags { get; set; } = new List(); 30 | 31 | public ICollection Revisions { get; set; } = new List(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/PostStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | public static class PostStatus 4 | { 5 | const string Published = "Published"; 6 | 7 | const string Draft = "Draft"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/Revision.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | public class Revision 4 | { 5 | public long AsOf { get; set; } 6 | 7 | public string Text { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | 6 | public class User 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; set; } 10 | 11 | public string EmailAddress { get; set; } 12 | 13 | public string PasswordHash { get; set; } 14 | 15 | public string FirstName { get; set; } 16 | 17 | public string LastName { get; set; } 18 | 19 | public string PreferredName { get; set; } 20 | 21 | public string Url { get; set; } 22 | 23 | public ICollection Authorizations { get; set; } = new List(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Entities/WebLog.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Entities 2 | { 3 | using Newtonsoft.Json; 4 | 5 | public class WebLog 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string Subtitle { get; set; } 13 | 14 | public string DefaultPage { get; set; } 15 | 16 | public string ThemePath { get; set; } 17 | 18 | public string UrlBase { get; set; } 19 | 20 | public string TimeZone { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Modules/HomeModule.cs: -------------------------------------------------------------------------------- 1 | namespace Dos.Modules 2 | { 3 | using Nancy; 4 | 5 | public class HomeModule : NancyModule 6 | { 7 | public HomeModule() : base() 8 | { 9 | Get("/", _ => "Hello World from Nancy C#"); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace Dos 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Nancy.Owin; 6 | 7 | public class Startup 8 | { 9 | public void Configure(IApplicationBuilder app) => 10 | app.UseOwin(x => x.UseNancy(options => options.Bootstrapper = new DosBootstrapper())); 11 | } 12 | } -------------------------------------------------------------------------------- /src/2-Nancy-CSharp/data-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hostname": "severus-server", 3 | "Database": "O2F2" 4 | } -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/App.fs: -------------------------------------------------------------------------------- 1 | namespace Tres 2 | 3 | open Microsoft.AspNetCore.Builder 4 | open Microsoft.AspNetCore.Hosting 5 | open Nancy 6 | open Nancy.Owin 7 | open RethinkDb.Driver.Net 8 | open System.IO 9 | 10 | type TresBootstrapper() = 11 | inherit DefaultNancyBootstrapper() 12 | 13 | override this.ConfigureApplicationContainer container = 14 | base.ConfigureApplicationContainer container 15 | let cfg = DataConfig.FromJson (File.ReadAllText "data-config.json") 16 | let conn = cfg.CreateConnection () 17 | conn.EstablishEnvironment cfg.Database |> Async.RunSynchronously 18 | container.Register conn |> ignore 19 | 20 | type Startup() = 21 | member this.Configure (app : IApplicationBuilder) = 22 | app.UseOwin (fun x -> x.UseNancy (fun opt -> opt.Bootstrapper <- new TresBootstrapper()) |> ignore) |> ignore 23 | 24 | module App = 25 | [] 26 | let main argv = 27 | use host = (new WebHostBuilder()).UseKestrel().UseStartup().Build() 28 | host.Run() 29 | 0 30 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/Data.fs: -------------------------------------------------------------------------------- 1 | namespace Tres 2 | 3 | open Newtonsoft.Json 4 | open RethinkDb.Driver 5 | open RethinkDb.Driver.Ast 6 | open RethinkDb.Driver.Net 7 | 8 | type DataConfig = 9 | { Hostname : string 10 | Port : int 11 | AuthKey : string 12 | Timeout : int 13 | Database : string 14 | } 15 | with 16 | member this.CreateConnection () : IConnection = 17 | let bldr = 18 | seq Connection.Builder> { 19 | yield fun builder -> match this.Hostname with null -> builder | host -> builder.Hostname host 20 | yield fun builder -> match this.Port with 0 -> builder | port -> builder.Port port 21 | yield fun builder -> match this.AuthKey with null -> builder | key -> builder.AuthKey key 22 | yield fun builder -> match this.Database with null -> builder | db -> builder.Db db 23 | yield fun builder -> match this.Timeout with 0 -> builder | timeout -> builder.Timeout timeout 24 | } 25 | |> Seq.fold (fun builder block -> block builder) (RethinkDB.R.Connection()) 26 | upcast bldr.Connect() 27 | static member FromJson json = JsonConvert.DeserializeObject json 28 | 29 | 30 | [] 31 | module Table = 32 | let Category = "Category" 33 | let Comment = "Comment" 34 | let Page = "Page" 35 | let Post = "Post" 36 | let User = "User" 37 | let WebLog = "WebLog" 38 | 39 | [] 40 | module DataExtensions = 41 | type IConnection with 42 | member this.EstablishEnvironment database = 43 | let r = RethinkDB.R 44 | let checkDatabase db = 45 | async { 46 | match db with 47 | | null 48 | | "" -> () 49 | | _ -> let! dbs = r.DbList().RunResultAsync this 50 | match dbs |> List.contains db with 51 | | true -> () 52 | | _ -> do! r.DbCreate(db).RunResultAsync this 53 | } 54 | let checkTables () = 55 | async { 56 | let! existing = r.TableList().RunResultAsync this 57 | [ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ] 58 | |> List.filter (fun tbl -> not (existing |> List.contains tbl)) 59 | |> List.map (fun tbl -> async { do! r.TableCreate(tbl).RunResultAsync this }) 60 | |> List.iter Async.RunSynchronously 61 | } 62 | let checkIndexes () = 63 | let indexesFor tbl = async { return! r.Table(tbl).IndexList().RunResultAsync this } 64 | let checkCategoryIndexes () = 65 | async { 66 | let! indexes = indexesFor Table.Category 67 | match indexes |> List.contains "WebLogId" with 68 | | true -> () 69 | | _ -> do! r.Table(Table.Category).IndexCreate("WebLogId").RunResultAsync this 70 | match indexes |> List.contains "WebLogAndSlug" with 71 | | true -> () 72 | | _ -> do! r.Table(Table.Category) 73 | .IndexCreate("WebLogAndSlug", ReqlFunction1(fun row -> upcast r.Array(row.["WebLogId"], row.["Slug"]))) 74 | .RunResultAsync this 75 | } 76 | let checkCommentIndexes () = 77 | async { 78 | let! indexes = indexesFor Table.Comment 79 | match indexes |> List.contains "PostId" with 80 | | true -> () 81 | | _ -> do! r.Table(Table.Comment).IndexCreate("PostId").RunResultAsync this 82 | } 83 | let checkPageIndexes () = 84 | async { 85 | let! indexes = indexesFor Table.Page 86 | match indexes |> List.contains "WebLogId" with 87 | | true -> () 88 | | _ -> do! r.Table(Table.Page).IndexCreate("WebLogId").RunResultAsync this 89 | match indexes |> List.contains "WebLogAndPermalink" with 90 | | true -> () 91 | | _ -> do! r.Table(Table.Page) 92 | .IndexCreate("WebLogAndPermalink", 93 | ReqlFunction1(fun row -> upcast r.Array(row.["WebLogId"], row.["Permalink"]))) 94 | .RunResultAsync this 95 | } 96 | let checkPostIndexes () = 97 | async { 98 | let! indexes = indexesFor Table.Post 99 | match indexes |> List.contains "WebLogId" with 100 | | true -> () 101 | | _ -> do! r.Table(Table.Post).IndexCreate("WebLogId").RunResultAsync this 102 | match indexes |> List.contains "Tags" with 103 | | true -> () 104 | | _ -> do! r.Table(Table.Post).IndexCreate("Tags").OptArg("multi", true).RunResultAsync this 105 | } 106 | let checkUserIndexes () = 107 | async { 108 | let! indexes = indexesFor Table.User 109 | match indexes |> List.contains "EmailAddress" with 110 | | true -> () 111 | | _ -> do! r.Table(Table.User).IndexCreate("EmailAddress").RunResultAsync this 112 | } 113 | async { 114 | do! checkCategoryIndexes () 115 | do! checkCommentIndexes () 116 | do! checkPageIndexes () 117 | do! checkPostIndexes () 118 | do! checkUserIndexes () 119 | } 120 | async { 121 | do! checkDatabase database 122 | do! checkTables () 123 | do! checkIndexes () 124 | } 125 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/Entities.fs: -------------------------------------------------------------------------------- 1 | namespace Tres.Entities 2 | 3 | open Newtonsoft.Json 4 | 5 | [] 6 | module AuthorizationLevel = 7 | [] 8 | let Administrator = "Administrator" 9 | [] 10 | let User = "User" 11 | 12 | [] 13 | module PostStatus = 14 | [] 15 | let Draft = "Draft" 16 | [] 17 | let Published = "Published" 18 | 19 | [] 20 | module CommentStatus = 21 | [] 22 | let Approved = "Approved" 23 | [] 24 | let Pending = "Pending" 25 | [] 26 | let Spam = "Spam" 27 | 28 | type Revision = { 29 | AsOf : int64 30 | Text : string 31 | } 32 | with 33 | static member Empty = 34 | { AsOf = 0L 35 | Text = "" 36 | } 37 | 38 | type Page = { 39 | [] 40 | Id : string 41 | WebLogId : string 42 | AuthorId : string 43 | Title : string 44 | Permalink : string 45 | PublishedOn : int64 46 | UpdatedOn : int64 47 | ShowInPageList : bool 48 | Text : string 49 | Revisions : Revision list 50 | } 51 | with 52 | static member Empty = 53 | { Id = "" 54 | WebLogId = "" 55 | AuthorId = "" 56 | Title = "" 57 | Permalink = "" 58 | PublishedOn = 0L 59 | UpdatedOn = 0L 60 | ShowInPageList = false 61 | Text = "" 62 | Revisions = [] 63 | } 64 | 65 | type WebLog = { 66 | [] 67 | Id : string 68 | Name : string 69 | Subtitle : string option 70 | DefaultPage : string 71 | ThemePath : string 72 | UrlBase : string 73 | TimeZone : string 74 | } 75 | with 76 | /// An empty web log 77 | static member Empty = 78 | { Id = "" 79 | Name = "" 80 | Subtitle = None 81 | DefaultPage = "" 82 | ThemePath = "default" 83 | UrlBase = "" 84 | TimeZone = "America/New_York" 85 | } 86 | 87 | type Authorization = { 88 | WebLogId : string 89 | Level : string 90 | } 91 | 92 | type User = { 93 | [] 94 | Id : string 95 | EmailAddress : string 96 | PasswordHash : string 97 | FirstName : string 98 | LastName : string 99 | PreferredName : string 100 | Url : string option 101 | Authorizations : Authorization list 102 | } 103 | with 104 | static member Empty = 105 | { Id = "" 106 | EmailAddress = "" 107 | FirstName = "" 108 | LastName = "" 109 | PreferredName = "" 110 | PasswordHash = "" 111 | Url = None 112 | Authorizations = [] 113 | } 114 | 115 | type Category = { 116 | [] 117 | Id : string 118 | WebLogId : string 119 | Name : string 120 | Slug : string 121 | Description : string option 122 | ParentId : string option 123 | Children : string list 124 | } 125 | with 126 | static member Empty = 127 | { Id = "new" 128 | WebLogId = "" 129 | Name = "" 130 | Slug = "" 131 | Description = None 132 | ParentId = None 133 | Children = [] 134 | } 135 | 136 | type Comment = { 137 | [] 138 | Id : string 139 | PostId : string 140 | InReplyToId : string option 141 | Name : string 142 | Email : string 143 | Url : string option 144 | Status : string 145 | PostedOn : int64 146 | Text : string 147 | } 148 | with 149 | static member Empty = 150 | { Id = "" 151 | PostId = "" 152 | InReplyToId = None 153 | Name = "" 154 | Email = "" 155 | Url = None 156 | Status = CommentStatus.Pending 157 | PostedOn = 0L 158 | Text = "" 159 | } 160 | 161 | type Post = { 162 | [] 163 | Id : string 164 | WebLogId : string 165 | AuthorId : string 166 | Status : string 167 | Title : string 168 | Permalink : string 169 | PublishedOn : int64 170 | UpdatedOn : int64 171 | Text : string 172 | CategoryIds : string list 173 | Tags : string list 174 | Revisions : Revision list 175 | } 176 | with 177 | static member Empty = 178 | { Id = "new" 179 | WebLogId = "" 180 | AuthorId = "" 181 | Status = PostStatus.Draft 182 | Title = "" 183 | Permalink = "" 184 | PublishedOn = 0L 185 | UpdatedOn = 0L 186 | Text = "" 187 | CategoryIds = [] 188 | Tags = [] 189 | Revisions = [] 190 | } 191 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/Extensions.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Tres.Extensions 3 | 4 | open System.Threading.Tasks 5 | 6 | // H/T: Suave 7 | type AsyncBuilder with 8 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 9 | /// a standard .NET task 10 | member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 11 | 12 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 13 | /// a standard .NET task which does not commpute a value 14 | member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 15 | 16 | member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t 17 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/HomeModule.fs: -------------------------------------------------------------------------------- 1 | namespace Tres 2 | 3 | open Nancy 4 | 5 | type HomeModule() as this = 6 | inherit NancyModule() 7 | 8 | do 9 | this.Get("/", fun _ -> "Hello World from Nancy F#") 10 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/Tres.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tres 5 | 1.0.0 6 | Exe 7 | netcoreapp2.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/3-Nancy-FSharp/data-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hostname": "severus-server", 3 | "Database": "O2F3" 4 | } -------------------------------------------------------------------------------- /src/4-Freya-FSharp/App.fs: -------------------------------------------------------------------------------- 1 | namespace Quatro 2 | 3 | open Freya.Core 4 | open Freya.Machines.Http 5 | open Freya.Routers.Uri.Template 6 | open Microsoft.AspNetCore.Builder 7 | open Microsoft.AspNetCore.Hosting 8 | open Quatro.Reader 9 | open System.IO 10 | 11 | module App = 12 | let lazyCfg = lazy (File.ReadAllText "data-config.json" |> DataConfig.FromJson) 13 | let cfg = lazyCfg.Force() 14 | let deps = { 15 | new IDependencies with 16 | member __.Conn 17 | with get () = 18 | let conn = lazy (cfg.CreateConnection ()) 19 | conn.Force() 20 | } 21 | 22 | let hello = 23 | freya { 24 | return Represent.text "Hello World from Freya" 25 | } 26 | 27 | let machine = 28 | freyaMachine { 29 | handleOk hello 30 | } 31 | 32 | let router = 33 | freyaRouter { 34 | resource "/" machine 35 | } 36 | 37 | type Startup () = 38 | member __.Configure (app : IApplicationBuilder) = 39 | let freyaOwin = OwinMidFunc.ofFreya router 40 | app.UseOwin (fun p -> p.Invoke freyaOwin) |> ignore 41 | 42 | [] 43 | let main _ = 44 | (*let initDb (conn : IConnection) = conn.EstablishEnvironment cfg.Database |> Async.RunSynchronously 45 | let start = liftDep getConn initDb 46 | start |> run deps *) 47 | liftDep getConn (Data.establishEnvironment cfg.Database >> Async.RunSynchronously) 48 | |> run deps 49 | use host = (new WebHostBuilder()).UseKestrel().UseStartup().Build() 50 | host.Run() 51 | 0 -------------------------------------------------------------------------------- /src/4-Freya-FSharp/Data.fs: -------------------------------------------------------------------------------- 1 | namespace Quatro 2 | 3 | open Chiron 4 | open RethinkDb.Driver 5 | open RethinkDb.Driver.Net 6 | open System 7 | 8 | type ConfigParameter = 9 | | Hostname of string 10 | | Port of int 11 | | AuthKey of string 12 | | Timeout of int 13 | | Database of string 14 | 15 | type DataConfig = { Parameters : ConfigParameter list } 16 | with 17 | member this.CreateConnection () : IConnection = 18 | let folder (builder : Connection.Builder) block = 19 | match block with 20 | | Hostname x -> builder.Hostname x 21 | | Port x -> builder.Port x 22 | | AuthKey x -> builder.AuthKey x 23 | | Timeout x -> builder.Timeout x 24 | | Database x -> builder.Db x 25 | let bldr = 26 | this.Parameters 27 | |> Seq.fold folder (RethinkDB.R.Connection ()) 28 | upcast bldr.Connect() 29 | member this.Database = 30 | match this.Parameters 31 | |> List.filter (fun x -> match x with Database _ -> true | _ -> false) 32 | |> List.tryHead with 33 | | Some (Database x) -> x 34 | | _ -> RethinkDBConstants.DefaultDbName 35 | static member FromJson json = 36 | match Json.parse json with 37 | | Object config -> 38 | let options = 39 | config 40 | |> Map.toList 41 | |> List.map (fun item -> 42 | match item with 43 | | "Hostname", String x -> Hostname x 44 | | "Port", Number x -> Port <| int x 45 | | "AuthKey", String x -> AuthKey x 46 | | "Timeout", Number x -> Timeout <| int x 47 | | "Database", String x -> Database x 48 | | key, value -> 49 | raise <| InvalidOperationException 50 | (sprintf "Unrecognized RethinkDB configuration parameter %s (value %A)" key value)) 51 | { Parameters = options } 52 | | _ -> { Parameters = [] } 53 | 54 | [] 55 | module Table = 56 | let Category = "Category" 57 | let Comment = "Comment" 58 | let Page = "Page" 59 | let Post = "Post" 60 | let User = "User" 61 | let WebLog = "WebLog" 62 | 63 | open RethinkDb.Driver.Ast 64 | 65 | [] 66 | module Data = 67 | let establishEnvironment database conn = 68 | let r = RethinkDB.R 69 | let checkDatabase db = 70 | async { 71 | match db with 72 | | null 73 | | "" -> () 74 | | _ -> let! dbs = r.DbList().RunResultAsync conn 75 | match dbs |> List.contains db with 76 | | true -> () 77 | | _ -> do! r.DbCreate(db).RunResultAsync conn 78 | } 79 | let checkTables () = 80 | async { 81 | let! existing = r.TableList().RunResultAsync conn 82 | [ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ] 83 | |> List.filter (fun tbl -> not (existing |> List.contains tbl)) 84 | |> List.map (fun tbl -> async { do! r.TableCreate(tbl).RunResultAsync conn }) 85 | |> List.iter Async.RunSynchronously 86 | } 87 | let checkIndexes () = 88 | let indexesFor tbl = async { return! r.Table(tbl).IndexList().RunResultAsync conn } 89 | let checkCategoryIndexes () = 90 | async { 91 | let! indexes = indexesFor Table.Category 92 | match indexes |> List.contains "WebLogId" with 93 | | true -> () 94 | | _ -> do! r.Table(Table.Category).IndexCreate("WebLogId").RunResultAsync conn 95 | match indexes |> List.contains "WebLogAndSlug" with 96 | | true -> () 97 | | _ -> do! r.Table(Table.Category) 98 | .IndexCreate("WebLogAndSlug", ReqlFunction1 (fun row -> upcast r.Array (row.["WebLogId"], row.["Slug"]))) 99 | .RunResultAsync conn 100 | } 101 | let checkCommentIndexes () = 102 | async { 103 | let! indexes = indexesFor Table.Comment 104 | match indexes |> List.contains "PostId" with 105 | | true -> () 106 | | _ -> do! r.Table(Table.Comment).IndexCreate("PostId").RunResultAsync conn 107 | } 108 | let checkPageIndexes () = 109 | async { 110 | let! indexes = indexesFor Table.Page 111 | match indexes |> List.contains "WebLogId" with 112 | | true -> () 113 | | _ -> do! r.Table(Table.Page).IndexCreate("WebLogId").RunResultAsync conn 114 | match indexes |> List.contains "WebLogAndPermalink" with 115 | | true -> () 116 | | _ -> do! r.Table(Table.Page) 117 | .IndexCreate("WebLogAndPermalink", 118 | ReqlFunction1(fun row -> upcast r.Array(row.["WebLogId"], row.["Permalink"]))) 119 | .RunResultAsync conn 120 | } 121 | let checkPostIndexes () = 122 | async { 123 | let! indexes = indexesFor Table.Post 124 | match indexes |> List.contains "WebLogId" with 125 | | true -> () 126 | | _ -> do! r.Table(Table.Post).IndexCreate("WebLogId").RunResultAsync conn 127 | match indexes |> List.contains "Tags" with 128 | | true -> () 129 | | _ -> do! r.Table(Table.Post).IndexCreate("Tags").OptArg("multi", true).RunResultAsync conn 130 | } 131 | let checkUserIndexes () = 132 | async { 133 | let! indexes = indexesFor Table.User 134 | match indexes |> List.contains "EmailAddress" with 135 | | true -> () 136 | | _ -> do! r.Table(Table.User).IndexCreate("EmailAddress").RunResultAsync conn 137 | } 138 | async { 139 | do! checkCategoryIndexes () 140 | do! checkCommentIndexes () 141 | do! checkPageIndexes () 142 | do! checkPostIndexes () 143 | do! checkUserIndexes () 144 | } 145 | async { 146 | do! checkDatabase database 147 | do! checkTables () 148 | do! checkIndexes () 149 | } 150 | -------------------------------------------------------------------------------- /src/4-Freya-FSharp/Dependencies.fs: -------------------------------------------------------------------------------- 1 | namespace Quatro 2 | 3 | open RethinkDb.Driver.Net 4 | 5 | // -- begin code lifted from #er demo -- 6 | type ReaderM<'d, 'out> = 'd -> 'out 7 | 8 | module Reader = 9 | // basic operations 10 | let run dep (rm : ReaderM<_,_>) = rm dep 11 | let constant (c : 'c) : ReaderM<_,'c> = fun _ -> c 12 | // lifting of functions and state 13 | let lift1 (f : 'd -> 'a -> 'out) : 'a -> ReaderM<'d, 'out> = fun a dep -> f dep a 14 | let lift2 (f : 'd -> 'a -> 'b -> 'out) : 'a -> 'b -> ReaderM<'d, 'out> = fun a b dep -> f dep a b 15 | let lift3 (f : 'd -> 'a -> 'b -> 'c -> 'out) : 'a -> 'b -> 'c -> ReaderM<'d, 'out> = fun a b c dep -> f dep a b c 16 | let liftDep (proj : 'd2 -> 'd1) (rm : ReaderM<'d1, 'output>) : ReaderM<'d2, 'output> = proj >> rm 17 | // functor 18 | let fmap (f : 'a -> 'b) (g : 'c -> 'a) : ('c -> 'b) = g >> f 19 | let map (f : 'a -> 'b) (rm : ReaderM<'d, 'a>) : ReaderM<'d,'b> = rm >> f 20 | let () = map 21 | // applicative-functor 22 | let apply (f : ReaderM<'d, 'a->'b>) (rm : ReaderM<'d, 'a>) : ReaderM<'d, 'b> = 23 | fun dep -> 24 | let f' = run dep f 25 | let a = run dep rm 26 | f' a 27 | let (<*>) = apply 28 | // monad 29 | let bind (rm : ReaderM<'d, 'a>) (f : 'a -> ReaderM<'d,'b>) : ReaderM<'d, 'b> = 30 | fun dep -> 31 | f (rm dep) 32 | |> run dep 33 | let (>>=) = bind 34 | type ReaderMBuilder internal () = 35 | member __.Bind(m, f) = m >>= f 36 | member __.Return(v) = constant v 37 | member __.ReturnFrom(v) = v 38 | member __.Delay(f) = f () 39 | let reader = ReaderMBuilder() 40 | // -- end code lifted from #er demo -- 41 | 42 | type IDependencies = 43 | abstract Conn : IConnection 44 | 45 | [] 46 | module DependencyExtraction = 47 | 48 | let getConn (deps : IDependencies) = deps.Conn 49 | -------------------------------------------------------------------------------- /src/4-Freya-FSharp/Entities.fs: -------------------------------------------------------------------------------- 1 | namespace Quatro.Entities 2 | 3 | open Newtonsoft.Json 4 | 5 | type CategoryId = CategoryId of string 6 | type CommentId = CommentId of string 7 | type PageId = PageId of string 8 | type PostId = PostId of string 9 | type UserId = UserId of string 10 | type WebLogId = WebLogId of string 11 | 12 | type Permalink = Permalink of string 13 | type Tag = Tag of string 14 | type Ticks = Ticks of int64 15 | type TimeZone = TimeZone of string 16 | type Url = Url of string 17 | 18 | type AuthorizationLevel = 19 | | Administrator 20 | | User 21 | 22 | type PostStatus = 23 | | Draft 24 | | Published 25 | 26 | type CommentStatus = 27 | | Approved 28 | | Pending 29 | | Spam 30 | 31 | type Revision = { 32 | AsOf : Ticks 33 | Text : string 34 | } 35 | with 36 | static member Empty = 37 | { AsOf = Ticks 0L 38 | Text = "" 39 | } 40 | 41 | type Page = { 42 | [] 43 | Id : PageId 44 | WebLogId : WebLogId 45 | AuthorId : UserId 46 | Title : string 47 | Permalink : Permalink 48 | PublishedOn : Ticks 49 | UpdatedOn : Ticks 50 | ShowInPageList : bool 51 | Text : string 52 | Revisions : Revision list 53 | } 54 | with 55 | static member Empty = 56 | { Id = PageId "" 57 | WebLogId = WebLogId "" 58 | AuthorId = UserId "" 59 | Title = "" 60 | Permalink = Permalink "" 61 | PublishedOn = Ticks 0L 62 | UpdatedOn = Ticks 0L 63 | ShowInPageList = false 64 | Text = "" 65 | Revisions = [] 66 | } 67 | 68 | type WebLog = { 69 | [] 70 | Id : WebLogId 71 | Name : string 72 | Subtitle : string option 73 | DefaultPage : string 74 | ThemePath : string 75 | UrlBase : string 76 | TimeZone : TimeZone 77 | } 78 | with 79 | /// An empty web log 80 | static member Empty = 81 | { Id = WebLogId "" 82 | Name = "" 83 | Subtitle = None 84 | DefaultPage = "" 85 | ThemePath = "default" 86 | UrlBase = "" 87 | TimeZone = TimeZone "America/New_York" 88 | } 89 | 90 | type Authorization = { 91 | WebLogId : WebLogId 92 | Level : AuthorizationLevel 93 | } 94 | 95 | type User = { 96 | [] 97 | Id : UserId 98 | EmailAddress : string 99 | PasswordHash : string 100 | FirstName : string 101 | LastName : string 102 | PreferredName : string 103 | Url : Url option 104 | Authorizations : Authorization list 105 | } 106 | with 107 | static member Empty = 108 | { Id = UserId "" 109 | EmailAddress = "" 110 | FirstName = "" 111 | LastName = "" 112 | PreferredName = "" 113 | PasswordHash = "" 114 | Url = None 115 | Authorizations = [] 116 | } 117 | 118 | type Category = { 119 | [] 120 | Id : CategoryId 121 | WebLogId : WebLogId 122 | Name : string 123 | Slug : string 124 | Description : string option 125 | ParentId : CategoryId option 126 | Children : CategoryId list 127 | } 128 | with 129 | static member Empty = 130 | { Id = CategoryId "new" 131 | WebLogId = WebLogId "" 132 | Name = "" 133 | Slug = "" 134 | Description = None 135 | ParentId = None 136 | Children = [] 137 | } 138 | 139 | type Comment = { 140 | [] 141 | Id : CommentId 142 | PostId : PostId 143 | InReplyToId : CommentId option 144 | Name : string 145 | EmailAddress : string 146 | Url : Url option 147 | Status : CommentStatus 148 | PostedOn : Ticks 149 | Text : string 150 | } 151 | with 152 | static member Empty = 153 | { Id = CommentId "" 154 | PostId = PostId "" 155 | InReplyToId = None 156 | Name = "" 157 | EmailAddress = "" 158 | Url = None 159 | Status = Pending 160 | PostedOn = Ticks 0L 161 | Text = "" 162 | } 163 | 164 | type Post = { 165 | [] 166 | Id : PostId 167 | WebLogId : WebLogId 168 | AuthorId : UserId 169 | Status : PostStatus 170 | Title : string 171 | Permalink : string 172 | PublishedOn : Ticks 173 | UpdatedOn : Ticks 174 | Text : string 175 | CategoryIds : CategoryId list 176 | Tags : Tag list 177 | Revisions : Revision list 178 | } 179 | with 180 | static member Empty = 181 | { Id = PostId "new" 182 | WebLogId = WebLogId "" 183 | AuthorId = UserId "" 184 | Status = Draft 185 | Title = "" 186 | Permalink = "" 187 | PublishedOn = Ticks 0L 188 | UpdatedOn = Ticks 0L 189 | Text = "" 190 | CategoryIds = [] 191 | Tags = [] 192 | Revisions = [] 193 | } 194 | -------------------------------------------------------------------------------- /src/4-Freya-FSharp/Extensions.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Quatro.Extensions 3 | 4 | open System.Threading.Tasks 5 | 6 | // H/T: Suave 7 | type AsyncBuilder with 8 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 9 | /// a standard .NET task 10 | member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 11 | 12 | /// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on 13 | /// a standard .NET task which does not commpute a value 14 | member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f) 15 | 16 | member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t 17 | -------------------------------------------------------------------------------- /src/4-Freya-FSharp/Quatro.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Quatro 5 | 1.0.0 6 | Exe 7 | netcoreapp2.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/4-Freya-FSharp/data-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hostname": "severus-server", 3 | "Database": "O2F4" 4 | } -------------------------------------------------------------------------------- /src/FromObjectsToFunctions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2002 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno", "1-AspNetCore-CSharp\Uno.csproj", "{EC0559BA-70CB-4C0C-A107-EC166EDBD25F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dos", "2-Nancy-CSharp\Dos.csproj", "{F11CF0E5-8AAA-486F-B197-6A7B83A7E3E3}" 9 | EndProject 10 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Tres", "3-Nancy-FSharp\Tres.fsproj", "{2A2E26E2-910F-4A21-B59B-F382AFCD6E26}" 11 | EndProject 12 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Quatro", "4-Freya-FSharp\Quatro.fsproj", "{E3C4F3FA-4D93-477A-9C71-3217A5258F99}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {EC0559BA-70CB-4C0C-A107-EC166EDBD25F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {EC0559BA-70CB-4C0C-A107-EC166EDBD25F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {EC0559BA-70CB-4C0C-A107-EC166EDBD25F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {EC0559BA-70CB-4C0C-A107-EC166EDBD25F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {F11CF0E5-8AAA-486F-B197-6A7B83A7E3E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {F11CF0E5-8AAA-486F-B197-6A7B83A7E3E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {F11CF0E5-8AAA-486F-B197-6A7B83A7E3E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {F11CF0E5-8AAA-486F-B197-6A7B83A7E3E3}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {2A2E26E2-910F-4A21-B59B-F382AFCD6E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {2A2E26E2-910F-4A21-B59B-F382AFCD6E26}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {2A2E26E2-910F-4A21-B59B-F382AFCD6E26}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {2A2E26E2-910F-4A21-B59B-F382AFCD6E26}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E3C4F3FA-4D93-477A-9C71-3217A5258F99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E3C4F3FA-4D93-477A-9C71-3217A5258F99}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E3C4F3FA-4D93-477A-9C71-3217A5258F99}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E3C4F3FA-4D93-477A-9C71-3217A5258F99}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {D1504A5F-DF14-4E39-B16E-C11E0C77D9FB} 42 | EndGlobalSection 43 | EndGlobal 44 | --------------------------------------------------------------------------------