Quatro - Step 4
Dependencies
Our dependencies don't change that much for this project; we won't change it to reference
Microsoft.AspNetCore.App
, but we'll add the RavenDB distributed cache, the MiniGuid
package, and the ASP.NET Core
session package. The new references are:
1: 2: 3: |
|
Then, run paket install
to register these as as part of this project.
The App.fs
File
For Uno, we made a lot of changes to Startup.cs
; since we initialized everything a bit more functionally, we'll
make these changes in App.fs
instead.
First up, we'll add some open
s:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
|
As with our others, we're adding certificate capability to the connection, and then initializing it into a variable, which allows us to use our initialized document store to create indexes, register it with DI, and use it to set up our session cache.
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
The above is pretty much a C#-to-F# translation of the remainder of ConfigureServices
from Uno, except for the
cookie name and the .AddGiraffe ()
call. The fact that the latter wasn't on our previous Giraffe steps is technically
an error, though this is the first step where it will bite us.
...then Testing
Before we tackle ConfigureApp
, we need to actually write the handlers that we need for this step. Our handlers won't
be one-liners, but they'll still be rather simple. First, create a file Handlers.fs
, and add it to the build order
just before App.fs
. Make the first line module Quatro.Handlers
, then we can write our session counter handler:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
While we're at it, we'll put in a stub for seed
as well, simply returning text "" next ctx
.
...then Back to App.fs
Now that we have our two handlers, we need to define routes for Giraffe, so it knows at which path each one should be executed. This is done through Giraffe's routing functions. The simplest set of functions that will get us our results is:
1: 2: 3: 4: 5: 6: |
|
Notice the type on webApp
- it's our familiar HttpHandler
. The >=>
operator is what lets us chain these
HttpHandler
s together. Defined as we did above, we have a nice central place to view all our routes. However, because
each of them is an HttpHandler
, we could just as easily define routers within handler modules, and attach them to a
route group. This is another case where composition gives us flexibility to structure things the way that makes the
most sense for the application.
With our router defined, we can now plug it into the application pipeline. We'll also enable sessions, and tie in a quick-and-dirty error handler (defined in our code, but not shown here).
1: 2: 3: 4: |
|
Data Seeding
The /seed
endpoint is familiar. However, as this was the step where we implemented single-case DUs to make our domain
less capable of representing an invalid state, there are several changes that will need to be made to the function as it
was defined in Tres. Review it to see
how we define these.
If we build and run the seed at this point, then look at our documents, something jumps right out at us. Our single-case DUs are serialized as...
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: |
|
This is better than Case
and Fields
pairs, but it's still not quite what we want; we want to be able to write a
query that says Where(fun x -> x.WebLogId = [our-id])
, not Where(fun x -> x.WebLogId.WebLogId = [our-id])
. Plus, the
entire reason we're using MiniGuid
s is to save space, and this takes up more space than we're saving!
Thankfully, though, Json.NET comes to the rescue once again. We can write some really simple converters that will
take care of our single-case DUs. Create Data.fs
and add it to the list of compiled files between Indexes.fs
and
Handlers.fs
. The top line should be module Quatro.Data
, then create a Converters
module below it; we'll put all
the converters there. The converters themselves don't have much to them; here are converters for Ticks
and
WebLogId
s:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
|
Once we get all our converters defined, we will define an all
property on the Converters
module. We'll also add the
FSharpLu converter to this list, so all
will be all the JsonConverter
s we need for our application, and we can use
them consistently throughout the application. (Interestingly, the default serialization of ArticleContent
is exactly
what we wrote for IArticleContent
in Tres, so it's the one type for which we don't need a converter here.)
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Remember our discussion from step 3 about converter order: Json.NET will pick the first converter in registration order that can convert a particular type it's trying to serialize or deserialize.
CompactUnionJsonConverter
converts anything that looks like a DU, so it needs to be last in the registration order.
Now, we need to let RavenDB know about all these new converters. In App.fs
, within the services
function, we can
remove the open
for Microsoft.FSharpLu.Json
, add one for Data
(our new module). The configuration of the JSON
serializers now looks like this:
1: 2: |
|
We won't be using it in this application, but Giraffe also uses Json.NET to serialize objects for JSON API requests. Using
Converters.all
to configure both RavenDB and Giraffe will ensure that, when we are looking at a JSON representation of our domain items, they are consistent and as we expect them to be.
Now, if we run our application, delete all the existing documents from O2F4
, and rerun the seed, we should see
documents that look more like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
...which is exactly what we want.
Conclusion
This is the most code we've written so far for this step. However, the majority of it has been at the seams between F# and the object-oriented .NET world. And, each of these pieces are small enough for us to completely understand. On top of that, the result is a system where, while illegal states could still be represented, it would be a lot more work; we are doing our best to make sure we fall into the pit of success.
type Categories_ByWebLogIdAndSlug =
inherit AbstractJavaScriptIndexCreationTask
new : unit -> Categories_ByWebLogIdAndSlug
--------------------
new : unit -> Categories_ByWebLogIdAndSlug
type AbstractJavaScriptIndexCreationTask =
inherit AbstractIndexCreationTask
member CreateIndexDefinition : unit -> IndexDefinition
member Fields : Dictionary<string, IndexFieldOptions> with get, set
member IsMapReduce : bool
member Maps : HashSet<string> with get, set
--------------------
AbstractJavaScriptIndexCreationTask() : AbstractJavaScriptIndexCreationTask
from Microsoft.FSharp.Core
union case WebLogId.WebLogId: string -> WebLogId
--------------------
type WebLogId = | WebLogId of string
val string : value:'T -> string
--------------------
type string = System.String
union case Ticks.Ticks: int64 -> Ticks
--------------------
type Ticks = | Ticks of int64
val int64 : value:'T -> int64 (requires member op_Explicit)
--------------------
type int64 = System.Int64
--------------------
type int64<'Measure> = int64
from Quatro.App
from Quatro
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
type X509Certificate2 =
inherit X509Certificate
new : unit -> X509Certificate2 + 12 overloads
member Archived : bool with get, set
member Extensions : X509ExtensionCollection
member FriendlyName : string with get, set
member GetNameInfo : nameType:X509NameType * forIssuer:bool -> string
member HasPrivateKey : bool
member Import : rawData:byte[] -> unit + 5 overloads
member IssuerName : X500DistinguishedName
member NotAfter : DateTime
member NotBefore : DateTime
...
--------------------
X509Certificate2() : X509Certificate2
(+0 other overloads)
X509Certificate2(rawData: byte []) : X509Certificate2
(+0 other overloads)
X509Certificate2(fileName: string) : X509Certificate2
(+0 other overloads)
X509Certificate2(handle: nativeint) : X509Certificate2
(+0 other overloads)
X509Certificate2(certificate: X509Certificate) : X509Certificate2
(+0 other overloads)
X509Certificate2(rawData: byte [], password: string) : X509Certificate2
(+0 other overloads)
X509Certificate2(rawData: byte [], password: System.Security.SecureString) : X509Certificate2
(+0 other overloads)
X509Certificate2(fileName: string, password: string) : X509Certificate2
(+0 other overloads)
X509Certificate2(fileName: string, password: System.Security.SecureString) : X509Certificate2
(+0 other overloads)
X509Certificate2(rawData: byte [], password: string, keyStorageFlags: X509KeyStorageFlags) : X509Certificate2
(+0 other overloads)
--------------------
new : ?tupleAsHeterogeneousArray:bool * ?usePropertyFormatterForValues:bool -> CompactUnionJsonConverter
static member CreateIndexes : assemblyToScan:Assembly * store:IDocumentStore * ?conventions:DocumentConventions * ?database:string -> unit + 1 overload
static member CreateIndexesAsync : assemblyToScan:Assembly * store:IDocumentStore * ?conventions:DocumentConventions * ?database:string * ?token:CancellationToken -> Task + 1 overload
IndexCreation.CreateIndexes(assemblyToScan: System.Reflection.Assembly, store: IDocumentStore,?conventions: Conventions.DocumentConventions,?database: string) : unit
(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
static member BackgroundColor : ConsoleColor with get, set
static member Beep : unit -> unit + 1 overload
static member BufferHeight : int with get, set
static member BufferWidth : int with get, set
static member CapsLock : bool
static member Clear : unit -> unit
static member CursorLeft : int with get, set
static member CursorSize : int with get, set
static member CursorTop : int with get, set
static member CursorVisible : bool with get, set
...
(+0 other overloads)
System.Console.WriteLine(value: string) : unit
(+0 other overloads)
System.Console.WriteLine(value: obj) : unit
(+0 other overloads)
System.Console.WriteLine(value: uint64) : unit
(+0 other overloads)
System.Console.WriteLine(value: int64) : unit
(+0 other overloads)
System.Console.WriteLine(value: uint32) : unit
(+0 other overloads)
System.Console.WriteLine(value: int) : unit
(+0 other overloads)
System.Console.WriteLine(value: float32) : unit
(+0 other overloads)
System.Console.WriteLine(value: float) : unit
(+0 other overloads)
System.Console.WriteLine(value: decimal) : unit
(+0 other overloads)
from Giraffe.HttpStatusCodeHandlers
from Quatro
from Giraffe.HttpStatusCodeHandlers
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
(extension) IApplicationBuilder.UseSession(options: SessionOptions) : IApplicationBuilder
module Data
from Quatro
--------------------
namespace Microsoft.FSharp.Data
from Quatro.Data
from Quatro
type TicksJsonConverter =
inherit JsonConverter<Ticks>
new : unit -> TicksJsonConverter
override ReadJson : r:JsonReader * Type * Ticks * bool * JsonSerializer -> Ticks
override WriteJson : w:JsonWriter * v:Ticks * JsonSerializer -> unit
JSON converter for Ticks
--------------------
new : unit -> TicksJsonConverter
type JsonConverter =
member CanConvert : objectType:Type -> bool
member CanRead : bool
member CanWrite : bool
member ReadJson : reader:JsonReader * objectType:Type * existingValue:obj * serializer:JsonSerializer -> obj
member WriteJson : writer:JsonWriter * value:obj * serializer:JsonSerializer -> unit
--------------------
type JsonConverter<'T> =
inherit JsonConverter
member CanConvert : objectType:Type -> bool
member ReadJson : reader:JsonReader * objectType:Type * existingValue:obj * serializer:JsonSerializer -> obj + 1 overload
member WriteJson : writer:JsonWriter * value:obj * serializer:JsonSerializer -> unit + 1 overload
--------------------
type JsonConverterAttribute =
inherit Attribute
new : converterType:Type -> JsonConverterAttribute + 1 overload
member ConverterParameters : obj[]
member ConverterType : Type
--------------------
JsonConverter() : JsonConverter
--------------------
JsonConverter() : JsonConverter<'T>
--------------------
JsonConverterAttribute(converterType: System.Type) : JsonConverterAttribute
JsonConverterAttribute(converterType: System.Type, [<System.ParamArray>] converterParameters: obj []) : JsonConverterAttribute
member AutoCompleteOnClose : bool with get, set
member Close : unit -> unit
member CloseAsync : ?cancellationToken:CancellationToken -> Task
member CloseOutput : bool with get, set
member Culture : CultureInfo with get, set
member DateFormatHandling : DateFormatHandling with get, set
member DateFormatString : string with get, set
member DateTimeZoneHandling : DateTimeZoneHandling with get, set
member FloatFormatHandling : FloatFormatHandling with get, set
member Flush : unit -> unit
...
type JsonSerializer =
new : unit -> JsonSerializer
member Binder : SerializationBinder with get, set
member CheckAdditionalContent : bool with get, set
member ConstructorHandling : ConstructorHandling with get, set
member Context : StreamingContext with get, set
member ContractResolver : IContractResolver with get, set
member Converters : JsonConverterCollection
member Culture : CultureInfo with get, set
member DateFormatHandling : DateFormatHandling with get, set
member DateFormatString : string with get, set
...
--------------------
JsonSerializer() : JsonSerializer
(+0 other overloads)
JsonWriter.WriteValue(value: System.Uri) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: byte []) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<System.TimeSpan>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<System.Guid>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<System.DateTimeOffset>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<System.DateTime>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<decimal>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<sbyte>) : unit
(+0 other overloads)
JsonWriter.WriteValue(value: System.Nullable<byte>) : unit
(+0 other overloads)
member Close : unit -> unit
member CloseInput : bool with get, set
member Culture : CultureInfo with get, set
member DateFormatString : string with get, set
member DateParseHandling : DateParseHandling with get, set
member DateTimeZoneHandling : DateTimeZoneHandling with get, set
member Depth : int
member FloatParseHandling : FloatParseHandling with get, set
member MaxDepth : Nullable<int> with get, set
member Path : string
...
type WebLogIdJsonConverter =
inherit JsonConverter<WebLogId>
new : unit -> WebLogIdJsonConverter
override ReadJson : r:JsonReader * Type * WebLogId * bool * JsonSerializer -> WebLogId
override WriteJson : w:JsonWriter * v:WebLogId * JsonSerializer -> unit
JSON converter for WebLogId
--------------------
new : unit -> WebLogIdJsonConverter
union case WebLogId.WebLogId: string -> WebLogId
--------------------
module WebLogId
from Quatro.Domain
--------------------
type WebLogId = | WebLogId of string
type JsonConverter =
member CanConvert : objectType:Type -> bool
member CanRead : bool
member CanWrite : bool
member ReadJson : reader:JsonReader * objectType:Type * existingValue:obj * serializer:JsonSerializer -> obj
member WriteJson : writer:JsonWriter * value:obj * serializer:JsonSerializer -> unit
--------------------
type JsonConverter<'T> =
inherit JsonConverter
member CanConvert : objectType:Type -> bool
member ReadJson : reader:JsonReader * objectType:Type * existingValue:obj * serializer:JsonSerializer -> obj + 1 overload
member WriteJson : writer:JsonWriter * value:obj * serializer:JsonSerializer -> unit + 1 overload
--------------------
type JsonConverterAttribute =
inherit Attribute
new : converterType:Type -> JsonConverterAttribute + 1 overload
member ConverterParameters : obj[]
member ConverterType : Type
--------------------
JsonConverterAttribute(converterType: System.Type) : JsonConverterAttribute
JsonConverterAttribute(converterType: System.Type, [<System.ParamArray>] converterParameters: obj []) : JsonConverterAttribute
val seq : sequence:seq<'T> -> seq<'T>
--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
interface IEnumerable
interface IEnumerable<'T>
member GetSlice : startIndex:int option * endIndex:int option -> 'T list
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
member Tail : 'T list
...