1
votes

When I save a new F# Record, I'm getting an extra column called Id@ in the RavenDb document, and it shows up when I load or view the object in code; it's even being converted to JSON through my F# API.

Here is my F# record type:

type Campaign = { mutable Id : string; name : string; description : string }

I'm not doing anything very exciting to save it:

let save c : Campaign =
    use session = store.OpenSession()
    session.Store(c)
    session.SaveChanges()
    c

Saving a new instance of a record creates a document with the Id of campaigns/289. Here is the full value of the document in RavenDb:

{
    "Id@": "campaigns/289",
    "name": "Recreating Id bug",
    "description": "Hello StackOverflow!"
}

Now, when I used this same database (and document) in C#, I didn't get the extra Id@ value. This is what a record looks like when I saved it in C#:

{
    "Description": "Hello StackOverflow!",
    "Name": "Look this worked fine",
}

(Aside - "name" vs "Name" means I have 2 name columns in my document. I understand that problem, at least).

So my question is: How do I get rid of the extra Id@ property being created when I save an F# record in RavenDb?

2
Wait, why is that column "extra"? I can clearly see it right there in your Campaign record definition. - Fyodor Soikin
Because the C# object definition also has an Id property, but it doesn't show up as a document value - it's the value used to access the document. The document created with F# has both the Id used to look up the specific database record, and a JSON value in the document called Id@; the C# version doesn't have the "extra" JSON value in the document. - Max

2 Answers

3
votes

This is a combination of... well, you can't quite call them "bugs", so let's say "non-straightforward features" in both F# compiler and RavenDb.

The F# compiler generates a public backing field for the Id record field. This field is named Id@ (a standard pattern for all F# backing fields), and it's public, because the record field is mutable. For immutable record fields, backing fields will be internal. Why it needs to generate a public backing field for mutable record fields, I don't know.

Now, RavenDb, when generating the schema, apparently looks at both properties and fields. This is a bit non-standard. The usual practice is to consider only properties. But alas, Raven picks up the public field named Id@, and makes it part of the schema.

You can combat this problem in two ways:

First, you could make the Id field immutable. I'm not sure whether that would work for you or RavenDb. Perhaps not, since the Id is probably generated on insert.

Second, you could declare your Campaign not as an F# record, but as a true class:

type Campaign( id: int, name: string, description: string ) = 
    member val Id = id with get, set
    member val name = name
    member val description = description

This way, all backing fields stay internal and no confusion will arise. The drawback is that you have to write every field twice: first as constructor argument, then as class member.

6
votes

As noted by Fyodor, this is caused by how F# generates a backing field when you create a record type. The default contract resolver for RavenDB serializes that backing field instead of the public property.

You can change the default contract resolver in ravendb. It will look something like this if you want to use the Newtonsoft Json.Net:

DocumentStore.Conventions.JsonContractResolver <- new CamelCasePropertyNamesContractResolver()

There is an explanation for why this works here (see the section titled: "The explanation"). Briefly, the Newtonsoft library uses the public properties of the type instead of the private backing fields.

I also recommend, instead of having the mutable property on the Id, you can put the [<CLIMutable>] attribute on the type itself like:

[<CLIMutable>]
type Campaign = { Id : string; name : string; description : string }

This makes it so libraries can mutate the values while preventing it in your code.