Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

NRT Support #25

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/on-push-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.2.0
with:
dotnet-version: 9.0.100
dotnet-version: 9.0.200

- name: Build & Test
run: make test config=Release
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on-push-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.2.0
with:
dotnet-version: 9.0.100
dotnet-version: 9.0.200

- name: Extract Version Suffix
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on-release-published.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.2.0
with:
dotnet-version: 9.0.100
dotnet-version: 9.0.200

- name: Download Release artifacts
uses: robinraju/release-downloader@v1.11
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Repository origins are:

## Supported platforms

This project targets `netstandard2.1` ([compatible runtimes](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-1#select-net-standard-version)).
This project targets `.net 8`, `.net 9` and `netstandard2.1` ([compatible runtimes](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-1#select-net-standard-version))

:warning: NRT support starts with `.net sdk 9.0.200`. F# compiler in .net sdk 9.0.10x does not set correctly nullable attributes on F# types. NRT are not supported on `netstandard2.1`.

## Contributing
* If you have a question about the library, then create an [issue][issues] with the `question` label.
Expand Down Expand Up @@ -89,7 +91,9 @@ NRT are serialized as:
* `null` if `Null`
* `object` if `NonNull` object

:warning: As of now, NRT can't be considered `null` when deserializing if value is missing.
On deserialization, missing value is mapped to `null`.

:warning: NRT support starts with .net sdk 9.0.200. F# compiler in .net sdk 9.0.10x does not set correctly nullable attributes on F# types.

# License
The contents of this library are made available under the [Apache License, Version 2.0][license].
Expand Down
3 changes: 2 additions & 1 deletion src/FSharp.MongoDB.Bson/FSharp.MongoDB.Bson.fsproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net9.0;net8.0;netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand All @@ -27,6 +27,7 @@

<ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
<!-- <ProjectReference Include="../../../mongo-csharp-driver/src/MongoDB.Bson/MongoDB.Bson.csproj" /> -->
</ItemGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ type FSharpRecordConvention() =
match classMap.ClassType with
| IsRecord typ ->
let fields = FSharpType.GetRecordFields(typ, bindingFlags)
let names = fields |> Array.map (fun x -> x.Name)
let names = fields |> Array.map _.Name

// Map the constructor of the record type.
let ctor = FSharpValue.PreComputeRecordConstructorInfo(typ, bindingFlags)
classMap.MapConstructor(ctor, names) |> ignore

// Map each field of the record type.
fields |> Array.iter (classMap.MapMember >> ignore)
fields |> Array.iter (mapMemberNullable classMap)
| _ -> ()
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type UnionCaseConvention() =

let mapUnionCase (classMap:BsonClassMap) (unionCase:UnionCaseInfo) =
let fields = unionCase.GetFields()
let names = fields |> Array.map (fun x -> x.Name)
let names = fields |> Array.map _.Name

classMap.SetDiscriminator unionCase.Name
classMap.SetDiscriminatorIsRequired true
Expand All @@ -76,7 +76,7 @@ type UnionCaseConvention() =
classMap.MapCreator(del, names) |> ignore

// Map each field of the union case.
fields |> Array.iter (classMap.MapMember >> ignore)
fields |> Array.iter (mapMemberNullable classMap)

interface IClassMapConvention with
member _.Apply classMap =
Expand Down
10 changes: 10 additions & 0 deletions src/FSharp.MongoDB.Bson/Serialization/FSharpTypeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ module private Helpers =

let bindingFlags = BindingFlags.Public ||| BindingFlags.NonPublic

#if !NETSTANDARD2_1
let nrtContext = NullabilityInfoContext()
#endif

/// <summary>
/// Returns <c>Some typ</c> when <c>pred typ</c> returns true, and <c>None</c> when
/// <c>pred typ</c> returns false.
Expand Down Expand Up @@ -90,3 +94,9 @@ module private Helpers =
/// Creates a generic type <c>'T</c> using the generic arguments of <c>typ</c>.
/// </summary>
let mkGenericUsingDef<'T> (typ:System.Type) = typ.GetGenericArguments() |> mkGeneric<'T>

/// <summary>
/// Maps a member of a <c>BsonClassMap</c> to a nullable value if possible.
/// </summary>
let mapMemberNullable (memberMap: BsonClassMap) (propertyInfo: PropertyInfo) =
memberMap.MapMember(propertyInfo) |> ignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module FSharp =
member _.GetSerializer typ =
let mkSerializer typ =
typ
|> Option.map (fun typ -> System.Activator.CreateInstance typ :?> IBsonSerializer)
|> Option.map (fun typ -> System.Activator.CreateInstance typ |> nonNull :?> IBsonSerializer)
|> Option.toObj

match typ with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type FSharpUnionSerializer<'T>() =
let mkClassMapSerializer (caseType:System.Type) =
let classMap = BsonClassMap.LookupClassMap caseType
let serializerType = typedefof<BsonClassMapSerializer<_>>.MakeGenericType [| caseType |]
System.Activator.CreateInstance(serializerType, classMap) :?> IBsonSerializer
System.Activator.CreateInstance(serializerType, classMap) |> nonNull :?> IBsonSerializer

// 8.5.4. Compiled Form of Union Types for Use from Other CLI Languages
// A compiled union type U has [o]ne CLI nested type U.C for each non-null union case C.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,97 @@ open NUnit.Framework

module FSharpNRTSerialization =

type UnionCase =
| Tuple of Int:int * String:(string | null)

type UnionCaseNotNull =
| TupleNotNull of Int:int * String:string

type Primitive =
{ String : string | null }
{ String : string | null
UnionCase: UnionCase
Int: int }

type RecordNoNull =
{ StringNotNull : string
Int: int }

type UnionCaseNoNull =
{ UnionCaseNotNull : UnionCaseNotNull
Int: int }


[<Test>]
let ``test serialize nullable reference (null) in a record type``() =
let value = { String = null }
let value = { String = null
UnionCase = Tuple(Int = 42, String = null)
Int = 42 }

let result = serialize value
let expected = BsonDocument([ BsonElement("String", BsonNull.Value) ])
let expected = BsonDocument([ BsonElement("String", BsonNull.Value)
BsonElement("UnionCase", BsonDocument([ BsonElement("_t", BsonString "Tuple")
BsonElement("Int", BsonInt32 42)
BsonElement("String", BsonNull.Value) ]))
BsonElement("Int", BsonInt32 42) ])

result |> should equal expected

[<Test>]
let ``test deserialize nullable reference (null) in a record type)``() =
// FIXME: this shall support deserializing missing null value for NRT
// as of now this means NRT can't be a missing value while deserializing
// let doc = BsonDocument()
let doc = BsonDocument([ BsonElement("String", BsonNull.Value) ])
// FIXME: once .net 9.0.200 is released, String can be omitted
let doc = BsonDocument([ BsonElement("String", BsonNull.Value)
BsonElement("UnionCase", BsonDocument([ BsonElement("_t", BsonString "Tuple")
BsonElement("Int", BsonInt32 42)
BsonElement("String", BsonNull.Value) ]))
BsonElement("Int", BsonInt32 42) ])

let result = deserialize<Primitive> doc
let expected = { String = null }
let expected = { String = null
UnionCase = Tuple(Int = 42, String = null)
Int = 42 }

result |> should equal expected

[<Test>]
let ``test serialize nullable reference (some) in a record type``() =
let value = { String = "A String" }
let value = { String = "A String"
UnionCase = Tuple(Int = 42, String = "Another String")
Int = 42 }

let result = serialize value
let expected = BsonDocument([ BsonElement("String", BsonString "A String") ])
let expected = BsonDocument([ BsonElement("String", BsonString "A String")
BsonElement("UnionCase", BsonDocument([ BsonElement("_t", BsonString "Tuple")
BsonElement("Int", BsonInt32 42)
BsonElement("String", BsonString "Another String") ]))
BsonElement("Int", BsonInt32 42) ])

result |> should equal expected

[<Test>]
let ``test deserialize nullable reference (some) in a record type``() =
let doc = BsonDocument([ BsonElement("String", BsonString "A String") ])
let doc = BsonDocument([ BsonElement("String", BsonString "A String")
BsonElement("UnionCase", BsonDocument([ BsonElement("_t", BsonString "Tuple")
BsonElement("Int", BsonInt32 42)
BsonElement("String", BsonString "Another String") ]))
BsonElement("Int", BsonInt32 42) ])

let result = deserialize<Primitive> doc
let expected = { String = "A String" }
let expected = { String = "A String"
UnionCase = Tuple(Int = 42, String = "Another String")
Int = 42 }

result |> should equal expected

[<Test>]
let ``test deserialize with missing non-null reference in record shall fail``() =
let doc = BsonDocument([ BsonElement("Int", BsonInt32 42) ])

(fun () -> deserialize<RecordNoNull> doc |> ignore)
|> should throw typeof<BsonSerializationException>

[<Test>]
let ``test deserialize with missing non-null reference in union case shall fail``() =
let doc = BsonDocument([ BsonElement("Int", BsonInt32 42) ])

(fun () -> deserialize<UnionCaseNoNull> doc |> ignore)
|> should throw typeof<BsonSerializationException>