diff --git a/.github/workflows/on-push-branch.yml b/.github/workflows/on-push-branch.yml index 0283d2e..3f81970 100644 --- a/.github/workflows/on-push-branch.yml +++ b/.github/workflows/on-push-branch.yml @@ -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 diff --git a/.github/workflows/on-push-tag.yml b/.github/workflows/on-push-tag.yml index ad59115..8991174 100644 --- a/.github/workflows/on-push-tag.yml +++ b/.github/workflows/on-push-tag.yml @@ -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: | diff --git a/.github/workflows/on-release-published.yml b/.github/workflows/on-release-published.yml index c58be85..4055927 100644 --- a/.github/workflows/on-release-published.yml +++ b/.github/workflows/on-release-published.yml @@ -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 diff --git a/README.md b/README.md index 8b6463d..0e37cea 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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]. diff --git a/src/FSharp.MongoDB.Bson/FSharp.MongoDB.Bson.fsproj b/src/FSharp.MongoDB.Bson/FSharp.MongoDB.Bson.fsproj index 5480523..820c4f8 100644 --- a/src/FSharp.MongoDB.Bson/FSharp.MongoDB.Bson.fsproj +++ b/src/FSharp.MongoDB.Bson/FSharp.MongoDB.Bson.fsproj @@ -1,7 +1,7 @@  - net9.0;netstandard2.1 + net9.0;net8.0;netstandard2.1 true enable true @@ -27,6 +27,7 @@ + diff --git a/src/FSharp.MongoDB.Bson/Serialization/Conventions/FSharpRecordConvention.fs b/src/FSharp.MongoDB.Bson/Serialization/Conventions/FSharpRecordConvention.fs index 60f228c..dc1f900 100644 --- a/src/FSharp.MongoDB.Bson/Serialization/Conventions/FSharpRecordConvention.fs +++ b/src/FSharp.MongoDB.Bson/Serialization/Conventions/FSharpRecordConvention.fs @@ -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) | _ -> () diff --git a/src/FSharp.MongoDB.Bson/Serialization/Conventions/UnionCaseConvention.fs b/src/FSharp.MongoDB.Bson/Serialization/Conventions/UnionCaseConvention.fs index fbd1112..fbc458d 100644 --- a/src/FSharp.MongoDB.Bson/Serialization/Conventions/UnionCaseConvention.fs +++ b/src/FSharp.MongoDB.Bson/Serialization/Conventions/UnionCaseConvention.fs @@ -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 @@ -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 = diff --git a/src/FSharp.MongoDB.Bson/Serialization/FSharpTypeHelpers.fs b/src/FSharp.MongoDB.Bson/Serialization/FSharpTypeHelpers.fs index 09104f1..d64b845 100644 --- a/src/FSharp.MongoDB.Bson/Serialization/FSharpTypeHelpers.fs +++ b/src/FSharp.MongoDB.Bson/Serialization/FSharpTypeHelpers.fs @@ -23,6 +23,10 @@ module private Helpers = let bindingFlags = BindingFlags.Public ||| BindingFlags.NonPublic +#if !NETSTANDARD2_1 + let nrtContext = NullabilityInfoContext() +#endif + /// /// Returns Some typ when pred typ returns true, and None when /// pred typ returns false. @@ -90,3 +94,9 @@ module private Helpers = /// Creates a generic type 'T using the generic arguments of typ. /// let mkGenericUsingDef<'T> (typ:System.Type) = typ.GetGenericArguments() |> mkGeneric<'T> + + /// + /// Maps a member of a BsonClassMap to a nullable value if possible. + /// + let mapMemberNullable (memberMap: BsonClassMap) (propertyInfo: PropertyInfo) = + memberMap.MapMember(propertyInfo) |> ignore diff --git a/src/FSharp.MongoDB.Bson/Serialization/FSharpValueSerializer.fs b/src/FSharp.MongoDB.Bson/Serialization/FSharpValueSerializer.fs index 3aae692..6fed8af 100644 --- a/src/FSharp.MongoDB.Bson/Serialization/FSharpValueSerializer.fs +++ b/src/FSharp.MongoDB.Bson/Serialization/FSharpValueSerializer.fs @@ -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 diff --git a/src/FSharp.MongoDB.Bson/Serialization/Serializers/FSharpUnionSerializer.fs b/src/FSharp.MongoDB.Bson/Serialization/Serializers/FSharpUnionSerializer.fs index 41ba739..ab0c60e 100644 --- a/src/FSharp.MongoDB.Bson/Serialization/Serializers/FSharpUnionSerializer.fs +++ b/src/FSharp.MongoDB.Bson/Serialization/Serializers/FSharpUnionSerializer.fs @@ -38,7 +38,7 @@ type FSharpUnionSerializer<'T>() = let mkClassMapSerializer (caseType:System.Type) = let classMap = BsonClassMap.LookupClassMap caseType let serializerType = typedefof>.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. diff --git a/tests/FSharp.MongoDB.Bson.Tests/Serialization/FSharpNRTSerializationTests.fs b/tests/FSharp.MongoDB.Bson.Tests/Serialization/FSharpNRTSerializationTests.fs index 4720c17..c682849 100644 --- a/tests/FSharp.MongoDB.Bson.Tests/Serialization/FSharpNRTSerializationTests.fs +++ b/tests/FSharp.MongoDB.Bson.Tests/Serialization/FSharpNRTSerializationTests.fs @@ -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 } + [] 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 [] 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 doc - let expected = { String = null } + let expected = { String = null + UnionCase = Tuple(Int = 42, String = null) + Int = 42 } result |> should equal expected [] 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 [] 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 doc - let expected = { String = "A String" } + let expected = { String = "A String" + UnionCase = Tuple(Int = 42, String = "Another String") + Int = 42 } result |> should equal expected + + [] + let ``test deserialize with missing non-null reference in record shall fail``() = + let doc = BsonDocument([ BsonElement("Int", BsonInt32 42) ]) + + (fun () -> deserialize doc |> ignore) + |> should throw typeof + + [] + let ``test deserialize with missing non-null reference in union case shall fail``() = + let doc = BsonDocument([ BsonElement("Int", BsonInt32 42) ]) + + (fun () -> deserialize doc |> ignore) + |> should throw typeof