objects |> functions


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: 
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.Options.ConfigurationExtensions
RavenDb.Client

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: 
{
  "RavenDB": {
    "Url": "http://localhost:8080",
    "Database": "O2F4"
  }
}

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: 
<Content Include="appsettings.json">
  <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

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: 
[<RequireQualifiedAccess>]
module Configure =
  
  open Microsoft.Extensions.Configuration
  open Microsoft.Extensions.DependencyInjection
  open Raven.Client.Documents
  
  let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
    cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath)
      .AddJsonFile("appsettings.json", optional = true, reloadOnChange = true)
      .AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName, optional = true)
      .AddEnvironmentVariables ()
    |> ignore
        
  let services (svc : IServiceCollection) =
    let config = svc.BuildServiceProvider().GetRequiredService<IConfiguration> ()
    let cfg = config.GetSection "RavenDB"
    let store = new DocumentStore (Urls = [| cfg.["Url"] |], Database = cfg.["Database"])
    svc.AddSingleton (store.Initialize ()) |> ignore

  let app (app : IApplicationBuilder) =
    app.UseGiraffe (htmlString "Hello World from Giraffe")

Then, within our main, we'll call these new functions; our host definition now looks like:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
use host =
  WebHostBuilder()
    .ConfigureAppConfiguration(Configure.configuration)
    .UseKestrel()
    .ConfigureServices(Configure.services)
    .Configure(System.Action<IApplicationBuilder> Configure.app)
    .Build ()

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: 
IndexCreation.CreateIndexes (typeof<Categories_ByWebLogIdAndSlug>.Assembly, store)

(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: 
  type ArticleContent =
    | Html     of string
    | Markdown of string

  module ArticleContent =
    let generate content =
      match content with Html x -> x | Markdown y -> MarkdownSharp.Markdown().Transform y

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 string Guid representation; this is what we'll use to define links to edit pages.
  • Converting a just a string Guid to a PageId; 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 the Page.Id field.
  • Creating a PageId from a Guid; 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: 
  type PageId = PageId of string
  module PageId =
    let create = Collection.idFor Collection.Page >> PageId
    let ofString (stringGuid : string) = create (Guid.Parse stringGuid)
    let toString x = match x with PageId y -> y
    let asGuidString x = ((toString >> Collection.fromId >> snd) x).ToString "N"

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 defined create as let 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: 
[<CLIMutable; NoComparison; NoEquality>]
type Page =
  { Id             : string
    // ...
    }
with
  static member Empty = 
    { Id             = ""
      // ...
      }

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: 
{
  "x":
    "Case": "Some",
    "Fields": [
      "Value": "abc123"
    ]
}

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: 
{
  "x": "abc123"
}

...or, in cases where x is None...

1: 
2: 
3: 
{
  "x": null
}

... (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: 
open Microsoft.FSharpLu.Json

...and add the following just above the call to svc.AddSingleton:

1: 
2: 
3: 
store.Conventions.CustomizeJsonDeserializer <-
  fun x ->
      x.Converters.Add (CompactUnionJsonConverter ())

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 JsonConverters; 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.


Back to Step 3

namespace Microsoft
namespace Giraffe
namespace Microsoft.AspNetCore
namespace Microsoft.AspNetCore.Builder
namespace Microsoft.AspNetCore.Hosting
Multiple items
type RequireQualifiedAccessAttribute =
  inherit Attribute
  new : unit -> RequireQualifiedAccessAttribute

--------------------
new : unit -> RequireQualifiedAccessAttribute
namespace Microsoft.Extensions
namespace Microsoft.Extensions.Configuration
namespace Microsoft.Extensions.DependencyInjection
namespace Raven
namespace Raven.Client
namespace Raven.Client.Documents
val configuration : ctx:WebHostBuilderContext -> cfg:IConfigurationBuilder -> unit
val ctx : WebHostBuilderContext
Multiple items
type WebHostBuilderContext =
  new : unit -> WebHostBuilderContext
  member Configuration : IConfiguration with get, set
  member HostingEnvironment : IHostingEnvironment with get, set

--------------------
WebHostBuilderContext() : WebHostBuilderContext
val cfg : IConfigurationBuilder
type IConfigurationBuilder =
  member Add : source:IConfigurationSource -> IConfigurationBuilder
  member Build : unit -> IConfigurationRoot
  member Properties : IDictionary<string, obj>
  member Sources : IList<IConfigurationSource>
(extension) IConfigurationBuilder.SetBasePath(basePath: string) : IConfigurationBuilder
property WebHostBuilderContext.HostingEnvironment: IHostingEnvironment
property IHostingEnvironment.ContentRootPath: string
val sprintf : format:Printf.StringFormat<'T> -> 'T
property IHostingEnvironment.EnvironmentName: string
val ignore : value:'T -> unit
val services : svc:IServiceCollection -> unit
val svc : IServiceCollection
type IServiceCollection =
  inherit IList<ServiceDescriptor>
  inherit ICollection<ServiceDescriptor>
  inherit IEnumerable<ServiceDescriptor>
  inherit IEnumerable
val config : IConfiguration
(extension) IServiceCollection.BuildServiceProvider() : ServiceProvider
(extension) IServiceCollection.BuildServiceProvider(validateScopes: bool) : ServiceProvider
(extension) IServiceCollection.BuildServiceProvider(options: ServiceProviderOptions) : ServiceProvider
type IConfiguration =
  member GetChildren : unit -> IEnumerable<IConfigurationSection>
  member GetReloadToken : unit -> IChangeToken
  member GetSection : key:string -> IConfigurationSection
  member Item : string -> string with get, set
val cfg : IConfigurationSection
IConfiguration.GetSection(key: string) : IConfigurationSection
val store : DocumentStore
Multiple items
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,'TImplementation (requires reference type and reference type and 'TImplementation :> 'TService)>() : IServiceCollection
(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
DocumentStore.Initialize() : IDocumentStore
val app : app:IApplicationBuilder -> unit
val app : IApplicationBuilder
type IApplicationBuilder =
  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
member IApplicationBuilder.UseGiraffe : handler:HttpHandler -> unit
val htmlString : html:string -> HttpHandler
val host : obj
namespace System
Multiple items
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
val typeof<'T> : System.Type
val Page : string
val idFor : coll:string -> docId:Guid -> string
val coll : string
val docId : Guid
Multiple items
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() : string
Guid.ToString(format: string) : string
Guid.ToString(format: string, provider: IFormatProvider) : string
val fromId : docId:string -> string * Guid
val docId : string
val parts : string []
val isNull : value:'T -> bool (requires 'T : null)
property Array.Length: int
Guid.Parse(input: string) : Guid
field Guid.Empty: Guid
Multiple items
type FormatException =
  inherit SystemException
  new : unit -> FormatException + 2 overloads

--------------------
FormatException() : FormatException
FormatException(message: string) : FormatException
FormatException(message: string, innerException: exn) : FormatException
module Domain

from Quatro
type ArticleContent =
  | Html of string
  | Markdown of string
union case ArticleContent.Html: string -> ArticleContent
Multiple items
val string : value:'T -> string

--------------------
type string = String
union case ArticleContent.Markdown: string -> ArticleContent
val generate : content:ArticleContent -> string
val content : ArticleContent
val x : string
val y : string
namespace MarkdownSharp
Multiple items
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
Multiple items
union case PageId.PageId: string -> PageId

--------------------
type PageId = | PageId of string
Multiple items
union case PageId.PageId: string -> PageId

--------------------
module PageId

from Quatro.Domain

--------------------
type PageId = | PageId of string
val create : (Guid -> PageId)
module Collection

from Quatro
val ofString : stringGuid:string -> PageId
val stringGuid : string
val toString : x:PageId -> string
val x : PageId
val asGuidString : x:PageId -> string
val snd : tuple:('T1 * 'T2) -> 'T2
Multiple items
type CLIMutableAttribute =
  inherit Attribute
  new : unit -> CLIMutableAttribute

--------------------
new : unit -> CLIMutableAttribute
Multiple items
type NoComparisonAttribute =
  inherit Attribute
  new : unit -> NoComparisonAttribute

--------------------
new : unit -> NoComparisonAttribute
Multiple items
type NoEqualityAttribute =
  inherit Attribute
  new : unit -> NoEqualityAttribute

--------------------
new : unit -> NoEqualityAttribute
Page.Id: string
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
Fork me on GitHub