Quatro - Step 3
Once again, we need to add the RavenDB dependency to paket.references
; we'll also need to add some ASP.NET Core
references as well. The new references are:
1: 2: 3: 4: |
|
Then, run paket install
to register these as as part of this project.
Configuring the Connection and Dependency Injection
As we're back in ASP.NET Core land, we'll do these together. We'll return to appsettings.json
for our configuration;
create that file with these values.
1: 2: 3: 4: 5: 6: |
|
We'll also need to make sure the file is copied to the build directory, so add the following to Quatro.fsproj
, inside
the list of compiled files:
1: 2: 3: |
|
In step 2, we mentioned that we are going to change most of our namespace declarations to
modules instead. In App.fs
, let's change namespace Quatro
to module Quatro.App
, remove the module App
declaration near the bottom of the file, and move let main
out to column 1. After that, we will convert the
initialization and DI logic to the function-based initialization we used in step 1. When the functions get pulled into
the WebHostBuilder
, their shapes may be a bit different than the Startup
class-based ones.
Here is what our new Configure
module looks like:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: |
|
Then, within our main
, we'll call these new functions; our host
definition now looks like:
1: 2: 3: 4: 5: 6: 7: |
|
Notice that we've moved our open
statements for types that we do not need outside of the Configure
module to appear
inside it. This is a way that F# can help isolate namespace conflicts. If you have ever worked on an application that
decided to name a type using a common term (Table
comes to my mind), the few namespaces you open, the less chance you
have of running into a competing definition of the same type.
Ensuring Indexes Exist
We'll bring across Indexes.fs
from Tres, changing it to Quatro
, and adding it to the compile order between
Domain.fs
and App.fs
. Then, we can go back to our Configure.services
function, and add the IndexCreation
call
to create our indexes:
1:
|
|
(We need to open Raven.Client.Documents.Indexes
and Indexes
(our module) for this call to work.)
Revising the Data Model
At this point, we need to change some things in our data model, before they bite us later on in the project. By
convention, when RavenDB persists POCOs, it looks for an Id
field of type string
, and if it does not find one, it
generates one within the document itself. (You can also define it as Guid
if you want, but that's not quite what
we're doing with our keys.) I did a lot of testing on various ways around not having to define identifiers with
Id : string
, but none of my tricks worked.
This will be another "seam" between F# representations and the .NET environment in general. However, we wouldn't be able
to get a single-case DU from an HTTP request either, so this gives us an opportunity to learn another common F# pattern.
When we defined the data model originally and replaced IArticleContent
with a multi-case
DU, I added a Generate
method to the DU's type, but mentioned that we may end up changing that. Let's do that now, as
we'll use a similar technique to develop a repeatable way to deal with these seams.
This common pattern is to use a type definition for the type itself, then define a module with the same name as the type
to hold the behavior associated with that type. The module will be compiled with the word Module
appended to the name
of the class; this used to require an attribute, but was such a common pattern, it became part of the way the language
works.
Here's our new ArticleContent
definition:
1: 2: 3: 4: 5: 6: 7: |
|
That's the pattern we'll use for our identifiers. To begin, create a file named Collection.fs
, and bring over the
Collection
module from Tres. There, we did not need it for our domain, but the modules for our Id types will rely
on it, so it needs to appear in the compilation order ahead of Domain.fs
. In Quatro.fsproj
, that's where it will go
- just before Domain.fs
. We'll also change IdFor
to idFor
and FromId
to fromId
.
(the result)
As we've used the Page
type for our examples thus far, we'll continue to use it for our example here. Our previous
definition of PageId
was type PageId = PageId of string
. As we think through the various edges and seams of the
project, there are five different transformations we'll need:
-
Converting a string PageId to a
PageId
; this is what we'll use if the web log has a static page defined as its home page. - Converting a
PageId
to just the stringGuid
representation; this is what we'll use to define links to edit pages. -
Converting a just a string
Guid
to aPageId
; this is what we'll use to grab the Id from a URL, and apply it to the proper collection. (We are fine if this bombs when given a non-GUID string; make invalid states unrepresentable...) - Converting a
PageId
to a string; this is what we'll use to set thePage.Id
field. - Creating a
PageId
from aGuid
; this will be used when the user creates a new page.
The first scenario is how the constructor works; PageId Pages/[guid-string]
is what we want there. The other four
requirements are implemented in the PageId
module:
1: 2: 3: 4: 5: 6: |
|
Thanks to the two functions we defined in the Collection
module, we can use composition and currying a good bit here.
We've seen the |>
operator before, the operator that sends the output of the function before it to the function after
it; the >>
operator does a similar thing, but at the function definition level.
create
demonstrates this; even though we did not specify any parameters on the function, its signature is
Guid -> PageId
. When we combine functions with >>
, its expected input will be the final input from the first
function; Collection.idFor
's signature is string -> Guid -> string
, but we have provided the first string, which (by
the rules of currying) makes the statement before the >>
have the signature Guid -> string
. When we combine via
>>
, the output of the first function will be applied as the last parameter of the next function. As PageId
is a
single-case DU, its constructor's signature is string -> PageId
. Since the return value of the first part is string
,
and the expected input for PageId
is string, we can combine them together. When we do, we get a function that has the
signature Guid -> PageId
.
Think of the difference between
|>
and>>
as the difference between execution and definition. We just as well could have definedcreate
aslet create x = Collection.idFor Collection.Page x |> PageId
, which implies that the function will execute first, then send its output to the next one; in our definition above, we describe a chain of functions that create a new function that just has one input and one output. Execution-wise, these are equivalent; in fact,let create x = PageId (Collection.IdFor Collection.Page x)
is also equivalent.One other note - sometimes, when composing via
>>
, the compiler may complain about a "value restriction." This means that, in the course of execution, the thing that appears to be a function will actually be executed once, and its result remembered, rather than executing each time. There is a good under-the-hood reason for this, but it can seem to be a strange quirk the first time you encounter it. To get around it, just specify the parameter and add it to the function body where it's needed.
Remember, "let the types be your guide;" if you're still unclear about how those four definitions work, hover over each
piece of them. You'll see the signatures for each of the functions that we chained together, and how the output of one
is the final input parameter of the next. snd
may be new; F# provides fst
and snd
to get the first and second
values in a tuple; since Collection.fromId
returns a string * Guid
tuple, snd
gives us the Guid
part.
We need to change the definition of Page
to use string
for the Id now.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
We will write similar modules for all of the Id types, and change the Ids in each type to strings; however, if an Id is
not the Id for that type, we will leave it as a DU. So, for example, Page.WebLogId
still has the WebLogId
type.
You may be thinking that this seems like boilerplate code that we thought F# helped us avoid; and, from one perspective,
it is. However, even though it's much more code, it defines how we move from the primitive-value world into the
strongly-typed environment of our application. And, while the presence of a PageId
module doesn't keep us from writing
let pg = { Page.empty with Id = "abc123" }
, it does provide an easy way for us to not do that. Just as we
developed the discipline of adding a new file to the project file when we created it, we will develop the discipline of
creating strongly-typed Ids in our request handlers the first time we address them, and only ever setting an Id field
using one of these functions, ideally deferring this to the point where it's stored in RavenDB. In effect, we'll push
all the primitives out to the edges of the application.
We won't need these for the other single-case DUs; however, there is another tweak we'll need to make for them.
Single-Case DUs to JSON
I mentioned on a prior page that Json.NET has great F# support. While this is true, it can generate rather verbose
output for some types, and discriminated unions are one of those. As an example, if we have a variable named x
defined
as a string option
that has the value abc123
, this will be serialized as...
1: 2: 3: 4: 5: 6: 7: |
|
There are times where this is exactly what we want. Imagine we had a DU case of type string * int * string
; this
structure is a great way to represent it in JSON. For single-case DUs, though (which Option<'T>
is), this is going
to make it very difficult to write a query based on the value of x
. In these cases, what we would prefer to see is
something like...
1: 2: 3: |
|
...or, in cases where x
is None
...
1: 2: 3: |
|
... (or even have x
excluded from the output).
There is a package called Microsoft.FSharpLu.Json that provides a Json.NET converter that handles these cases; and,
since RavenDB uses Json.NET to serialize the documents, all we have to do is tell it to use it. This will be a new
reference overall, so we'll need to add nuget Microsoft.FSharpLu.Json
to paket.dependencies
, and then add
Microsoft.FSharpLu.Json
to paket.references
in Quatro. (paket install
as usual.)
Then, in App.fs
, we'll need to open the namespace (within the Configure
module):
1:
|
|
...and add the following just above the call to svc.AddSingleton
:
1: 2: 3: |
|
This will serialize the single-case DUs as we described above, including our Id fields that aren't the actual Id of a document (those that point to other documents; our foreign keys, in relational terms). Additionally, our multi-case DUs that have no associated types will be serialized as strings, so statuses and levels will look just as though we were still using a defined set of magic strings.
As we move along, we may need to write other JsonConverter
s; if we do, we'll just need to add them to the function
above. One important thing to remember is that Json.NET will use the first matching converter it finds; so, if we write
a converter for a DU, the new one will need to go above the CompactUnionJsonConverter
or that one will be used
instead.
Congratulations - Quatro should be ready to go! Ensure you've created an O2F4 database in RavenDB, then dotnet run
this project and ensure that the store is initialized properly, and all indexes are created the way they were for our
previous versions. And, know that we'll carry every bit of this hard work forward to Cinco;
replacing DI will be our learning activity there.
type RequireQualifiedAccessAttribute =
inherit Attribute
new : unit -> RequireQualifiedAccessAttribute
--------------------
new : unit -> RequireQualifiedAccessAttribute
type WebHostBuilderContext =
new : unit -> WebHostBuilderContext
member Configuration : IConfiguration with get, set
member HostingEnvironment : IHostingEnvironment with get, set
--------------------
WebHostBuilderContext() : WebHostBuilderContext
member Add : source:IConfigurationSource -> IConfigurationBuilder
member Build : unit -> IConfigurationRoot
member Properties : IDictionary<string, obj>
member Sources : IList<IConfigurationSource>
inherit IList<ServiceDescriptor>
inherit ICollection<ServiceDescriptor>
inherit IEnumerable<ServiceDescriptor>
inherit IEnumerable
(extension) IServiceCollection.BuildServiceProvider(validateScopes: bool) : ServiceProvider
(extension) IServiceCollection.BuildServiceProvider(options: ServiceProviderOptions) : ServiceProvider
member GetChildren : unit -> IEnumerable<IConfigurationSection>
member GetReloadToken : unit -> IChangeToken
member GetSection : key:string -> IConfigurationSection
member Item : string -> string with get, set
type DocumentStore =
inherit DocumentStoreBase
new : unit -> DocumentStore
member AggressivelyCacheFor : cacheDuration:TimeSpan * ?database:string -> IDisposable + 1 overload
member BulkInsert : ?database:string * ?token:CancellationToken -> BulkInsertOperation
member Changes : ?database:string -> IDatabaseChanges + 1 overload
member DisableAggressiveCaching : ?database:string -> IDisposable
member Dispose : unit -> unit
member GetLastDatabaseChangesStateException : ?database:string * ?nodeTag:string -> Exception
member GetRequestExecutor : ?database:string -> RequestExecutor
member Identifier : string with get, set
member Initialize : unit -> IDocumentStore
...
--------------------
DocumentStore() : DocumentStore
(extension) IServiceCollection.AddSingleton<'TService (requires reference type)>() : IServiceCollection
(extension) IServiceCollection.AddSingleton(serviceType: System.Type) : IServiceCollection
(extension) IServiceCollection.AddSingleton<'TService (requires reference type)>(implementationFactory: System.Func<System.IServiceProvider,'TService>) : IServiceCollection
(extension) IServiceCollection.AddSingleton<'TService,'TImplementation (requires reference type and reference type and 'TImplementation :> 'TService)>(implementationFactory: System.Func<System.IServiceProvider,'TImplementation>) : IServiceCollection
(extension) IServiceCollection.AddSingleton<'TService (requires reference type)>(implementationInstance: 'TService) : IServiceCollection
(extension) IServiceCollection.AddSingleton(serviceType: System.Type, implementationType: System.Type) : IServiceCollection
(extension) IServiceCollection.AddSingleton(serviceType: System.Type, implementationFactory: System.Func<System.IServiceProvider,obj>) : IServiceCollection
(extension) IServiceCollection.AddSingleton(serviceType: System.Type, implementationInstance: obj) : IServiceCollection
member ApplicationServices : IServiceProvider with get, set
member Build : unit -> RequestDelegate
member New : unit -> IApplicationBuilder
member Properties : IDictionary<string, obj>
member ServerFeatures : IFeatureCollection
member Use : middleware:Func<RequestDelegate, RequestDelegate> -> IApplicationBuilder
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15,'T16> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 * 'T16 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6,'T7> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5,'T6> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4,'T5> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 -> unit
--------------------
type Action<'T1,'T2,'T3,'T4> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 -> unit
--------------------
type Action<'T1,'T2,'T3> =
delegate of 'T1 * 'T2 * 'T3 -> unit
--------------------
type Action<'T1,'T2> =
delegate of 'T1 * 'T2 -> unit
--------------------
type Action =
delegate of unit -> unit
--------------------
type Action<'T> =
delegate of 'T -> unit
type Guid =
struct
new : b:byte[] -> Guid + 4 overloads
member CompareTo : value:obj -> int + 1 overload
member Equals : o:obj -> bool + 1 overload
member GetHashCode : unit -> int
member ToByteArray : unit -> byte[]
member ToString : unit -> string + 2 overloads
static val Empty : Guid
static member NewGuid : unit -> Guid
static member Parse : input:string -> Guid
static member ParseExact : input:string * format:string -> Guid
...
end
--------------------
Guid ()
Guid(b: byte []) : Guid
Guid(g: string) : Guid
Guid(a: int, b: int16, c: int16, d: byte []) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid.ToString(format: string) : string
Guid.ToString(format: string, provider: IFormatProvider) : string
type FormatException =
inherit SystemException
new : unit -> FormatException + 2 overloads
--------------------
FormatException() : FormatException
FormatException(message: string) : FormatException
FormatException(message: string, innerException: exn) : FormatException
from Quatro
| Html of string
| Markdown of string
val string : value:'T -> string
--------------------
type string = String
type Markdown =
new : unit -> Markdown + 2 overloads
member AsteriskIntraWordEmphasis : bool with get, set
member AutoHyperlink : bool with get, set
member AutoNewLines : bool with get, set
member EmptyElementSuffix : string with get, set
member LinkEmails : bool with get, set
member StrictBoldItalic : bool with get, set
member Transform : text:string -> string
member Version : string
--------------------
MarkdownSharp.Markdown() : MarkdownSharp.Markdown
MarkdownSharp.Markdown(loadOptionsFromConfigFile: bool) : MarkdownSharp.Markdown
MarkdownSharp.Markdown(options: MarkdownSharp.MarkdownOptions) : MarkdownSharp.Markdown
union case PageId.PageId: string -> PageId
--------------------
type PageId = | PageId of string
union case PageId.PageId: string -> PageId
--------------------
module PageId
from Quatro.Domain
--------------------
type PageId = | PageId of string
from Quatro
type CLIMutableAttribute =
inherit Attribute
new : unit -> CLIMutableAttribute
--------------------
new : unit -> CLIMutableAttribute
type NoComparisonAttribute =
inherit Attribute
new : unit -> NoComparisonAttribute
--------------------
new : unit -> NoComparisonAttribute
type NoEqualityAttribute =
inherit Attribute
new : unit -> NoEqualityAttribute
--------------------
new : unit -> NoEqualityAttribute
val string : value:'T -> string
--------------------
type string = System.String