├── .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 |