diff --git a/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs b/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs index 958fc6980..0bb778c95 100644 --- a/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs +++ b/src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs @@ -150,6 +150,17 @@ module internal ProviderHelpers = member x.Inverse(denominator) : Type = ProvidedMeasureBuilder.Inverse(denominator) } +#if NET6_0_OR_GREATER + /// Returns true when the target runtime assembly is a .NET 6+ build and therefore + /// supports System.DateOnly / System.TimeOnly in generated types. + let runtimeSupportsNet6Types (runtimeAssemblyPath: string) = + // The assembly path contains the TFM, e.g. "…/net8.0/FSharp.Data.dll". + // Anything matching "/net." where N ≥ 6 is a net6+ target. + let path = runtimeAssemblyPath.Replace('\\', '/').ToLowerInvariant() + let m = System.Text.RegularExpressions.Regex.Match(path, @"/net(\d+)\.") + m.Success && (int m.Groups.[1].Value) >= 6 +#endif + let asyncMap (resultType: Type) (valueAsync: Expr>) (body: Expr<'T> -> Expr) = let (?) = QuotationBuilder.(?) let convFunc = ReflectionHelpers.makeDelegate (Expr.Cast >> body) typeof<'T> diff --git a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs index daf5ea51b..802ca013e 100644 --- a/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs +++ b/src/FSharp.Data.DesignTime/Csv/CsvProvider.fs @@ -103,16 +103,23 @@ type public CsvProvider(cfg: TypeProviderConfig) as this = let inferredFields = use _holder = IO.logTime "Inference" sample - sampleCsv.InferColumnTypes( - inferRows, - TextRuntime.GetMissingValues missingValuesStr, - inferenceMode, - TextRuntime.GetCulture cultureStr, - schema, - assumeMissingValues, - preferOptionals, - unitsOfMeasureProvider - ) + let fields = + sampleCsv.InferColumnTypes( + inferRows, + TextRuntime.GetMissingValues missingValuesStr, + inferenceMode, + TextRuntime.GetCulture cultureStr, + schema, + assumeMissingValues, + preferOptionals, + unitsOfMeasureProvider + ) +#if NET6_0_OR_GREATER + if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then fields + else fields |> List.map StructuralInference.downgradeNet6PrimitiveProperty +#else + fields +#endif use _holder = IO.logTime "TypeGeneration" sample diff --git a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs index 3d3625d56..edd0db0c6 100644 --- a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs +++ b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs @@ -37,10 +37,11 @@ module internal HtmlGenerator = let private createTableType getTableTypeName (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types (table: HtmlTable) = - let columns = + let rawColumns = match table.InferedProperties with | Some inferedProperties -> inferedProperties | None -> @@ -52,6 +53,14 @@ module internal HtmlGenerator = else table.Rows) + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then rawColumns + else rawColumns |> List.map StructuralInference.downgradeNet6PrimitiveProperty +#else + rawColumns +#endif + let fields = columns |> List.mapi (fun index field -> @@ -128,9 +137,17 @@ module internal HtmlGenerator = create, tableType - let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) (list: HtmlList) = + let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) supportsNet6Types (list: HtmlList) = - let columns = HtmlInference.inferListType inferenceParameters list.Values + let rawColumns = HtmlInference.inferListType inferenceParameters list.Values + + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then rawColumns + else StructuralInference.downgradeNet6Types rawColumns +#else + rawColumns +#endif let listItemType, conv = match columns with @@ -185,6 +202,7 @@ module internal HtmlGenerator = let private createDefinitionListType getDefinitionListTypeName (inferenceParameters, missingValuesStr, cultureStr) + supportsNet6Types (definitionList: HtmlDefinitionList) = @@ -192,7 +210,15 @@ module internal HtmlGenerator = let createListType index (list: HtmlList) = - let columns = HtmlInference.inferListType inferenceParameters list.Values + let rawColumns = HtmlInference.inferListType inferenceParameters list.Values + + let columns = +#if NET6_0_OR_GREATER + if supportsNet6Types then rawColumns + else StructuralInference.downgradeNet6Types rawColumns +#else + rawColumns +#endif let listItemType, conv = match columns with @@ -264,7 +290,7 @@ module internal HtmlGenerator = definitionListType - let generateTypes asm ns typeName parameters htmlObjects = + let generateTypes asm ns typeName parameters supportsNet6Types htmlObjects = let htmlType = ProvidedTypeDefinition( @@ -303,7 +329,7 @@ module internal HtmlGenerator = match htmlObj with | Table table -> let containerType = getOrCreateContainer "Tables" - let create, tableType = createTableType getTypeName parameters table + let create, tableType = createTableType getTypeName parameters supportsNet6Types table htmlType.AddMember tableType containerType.AddMember @@ -315,7 +341,7 @@ module internal HtmlGenerator = | List list -> let containerType = getOrCreateContainer "Lists" - let create, tableType = createListType getTypeName parameters list + let create, tableType = createListType getTypeName parameters supportsNet6Types list htmlType.AddMember tableType containerType.AddMember @@ -326,7 +352,7 @@ module internal HtmlGenerator = ) | DefinitionList definitionList -> let containerType = getOrCreateContainer "DefinitionLists" - let tableType = createDefinitionListType getTypeName parameters definitionList + let tableType = createDefinitionListType getTypeName parameters supportsNet6Types definitionList htmlType.AddMember tableType containerType.AddMember diff --git a/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs b/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs index 62828d91e..7cb3c17fd 100644 --- a/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Html/HtmlProvider.fs @@ -62,9 +62,15 @@ type public HtmlProvider(cfg: TypeProviderConfig) as this = PreferOptionals = preferOptionals InferenceMode = inferenceMode } +#if NET6_0_OR_GREATER + let supportsNet6Types = ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly +#else + let supportsNet6Types = false +#endif + doc |> HtmlRuntime.getHtmlObjects (Some inferenceParameters) includeLayoutTables - |> HtmlGenerator.generateTypes asm ns typeName (inferenceParameters, missingValuesStr, cultureStr) + |> HtmlGenerator.generateTypes asm ns typeName (inferenceParameters, missingValuesStr, cultureStr) supportsNet6Types use _holder = IO.logTime "TypeGeneration" sample diff --git a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs index de6c2f5e4..c9278ea11 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs @@ -76,28 +76,35 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" (if schema <> "" then schema else sample) - if schema <> "" then - // Use the JSON Schema for type inference - use _holder = IO.logTime "SchemaInference" schema - - let schemaValue = JsonValue.Parse(value) - let jsonSchema = JsonSchema.parseSchema schemaValue - JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema - else - // Use sample-based inference - let samples = - use _holder = IO.logTime "Parsing" sample - - if sampleIsList then - JsonDocument.CreateList(new StringReader(value)) - |> Array.map (fun doc -> doc.JsonValue) - else - [| JsonValue.Parse(value) |] - - samples - |> Array.map (fun sampleJson -> - JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson) - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + let rawInfered = + if schema <> "" then + // Use the JSON Schema for type inference + use _holder = IO.logTime "SchemaInference" schema + + let schemaValue = JsonValue.Parse(value) + let jsonSchema = JsonSchema.parseSchema schemaValue + JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema + else + // Use sample-based inference + let samples = + use _holder = IO.logTime "Parsing" sample + + if sampleIsList then + JsonDocument.CreateList(new StringReader(value)) + |> Array.map (fun doc -> doc.JsonValue) + else + [| JsonValue.Parse(value) |] + + samples + |> Array.map (fun sampleJson -> + JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson) + |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top +#if NET6_0_OR_GREATER + if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then rawInfered + else StructuralInference.downgradeNet6Types rawInfered +#else + rawInfered +#endif use _holder = IO.logTime "TypeGeneration" (if schema <> "" then schema else sample) diff --git a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs index dfbf2e766..30039d633 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs @@ -74,7 +74,13 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" sample - schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements + let t = schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements +#if NET6_0_OR_GREATER + if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then t + else StructuralInference.downgradeNet6Types t +#else + t +#endif use _holder = IO.logTime "TypeGeneration" sample @@ -113,14 +119,21 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let inferedType = use _holder = IO.logTime "Inference" sample - samples - |> XmlInference.inferType - unitsOfMeasureProvider - inferenceMode - (TextRuntime.GetCulture cultureStr) - false - globalInference - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + let t = + samples + |> XmlInference.inferType + unitsOfMeasureProvider + inferenceMode + (TextRuntime.GetCulture cultureStr) + false + globalInference + |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top +#if NET6_0_OR_GREATER + if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then t + else StructuralInference.downgradeNet6Types t +#else + t +#endif use _holder = IO.logTime "TypeGeneration" sample diff --git a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs index 0ca9a763a..df70c6139 100644 --- a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs +++ b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs @@ -623,3 +623,63 @@ let inferPrimitiveType [] let getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit = inferPrimitiveType unitsOfMeasureProvider inferenceMode cultureInfo value unit + +#if NET6_0_OR_GREATER +/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan throughout an InferedType tree. +/// Used in design-time code when the target framework does not support these .NET 6+ types. +let internal downgradeNet6Types (inferedType: InferedType) : InferedType = + let downgradeTag tag = + match tag with + | InferedTypeTag.DateOnly -> InferedTypeTag.DateTime + | InferedTypeTag.TimeOnly -> InferedTypeTag.TimeSpan + | _ -> tag + + let downgradeType (typ: Type) = + if typ = typeof then typeof + elif typ = typeof then typeof + else typ + + // Use reference-equality-based visited set to handle cyclic InferedType graphs + // (e.g. recursive XML schemas). When a cycle is detected we return the original node. + let visited = + System.Collections.Generic.HashSet( + { new System.Collections.Generic.IEqualityComparer with + member _.Equals(x, y) = obj.ReferenceEquals(x, y) + member _.GetHashCode(x) = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(x) }) + + let rec convert infType = + if not (visited.Add(infType)) then + infType // cycle detected – return original to avoid infinite recursion + else + let result = + match infType with + | InferedType.Primitive(typ, unit, optional, overrideOnMerge) -> + InferedType.Primitive(downgradeType typ, unit, optional, overrideOnMerge) + | InferedType.Record(name, props, optional) -> + InferedType.Record(name, props |> List.map (fun p -> { p with Type = convert p.Type }), optional) + | InferedType.Collection(order, types) -> + InferedType.Collection( + order |> List.map downgradeTag, + types |> Map.toSeq |> Seq.map (fun (k, (m, t)) -> downgradeTag k, (m, convert t)) |> Map.ofSeq) + | InferedType.Heterogeneous(types, containsOptional) -> + InferedType.Heterogeneous( + types |> Map.toSeq |> Seq.map (fun (k, t) -> downgradeTag k, convert t) |> Map.ofSeq, + containsOptional) + | InferedType.Json(innerType, optional) -> InferedType.Json(convert innerType, optional) + | _ -> infType + result + + convert inferedType + +/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan in a PrimitiveInferedProperty. +/// Used in design-time code when the target framework does not support these .NET 6+ types. +let internal downgradeNet6PrimitiveProperty (field: StructuralTypes.PrimitiveInferedProperty) = + let v = field.Value + + if v.InferedType = typeof then + { field with Value = { v with InferedType = typeof; RuntimeType = typeof } } + elif v.InferedType = typeof then + { field with Value = { v with InferedType = typeof; RuntimeType = typeof } } + else + field +#endif diff --git a/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj b/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj index ecc8f9f67..be6b0aba0 100644 --- a/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj +++ b/src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.0;net8.0 $(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44 true false diff --git a/src/FSharp.Data.Xml.Core/XsdInference.fs b/src/FSharp.Data.Xml.Core/XsdInference.fs index 9fe3a7899..a25f6b787 100644 --- a/src/FSharp.Data.Xml.Core/XsdInference.fs +++ b/src/FSharp.Data.Xml.Core/XsdInference.fs @@ -214,7 +214,11 @@ module internal XsdInference = function | XmlTypeCode.Int -> typeof | XmlTypeCode.Long -> typeof +#if NET6_0_OR_GREATER + | XmlTypeCode.Date -> typeof +#else | XmlTypeCode.Date -> typeof +#endif | XmlTypeCode.DateTime -> typeof | XmlTypeCode.Boolean -> typeof | XmlTypeCode.Decimal -> typeof diff --git a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs index 539f539e0..7b2795697 100644 --- a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs +++ b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs @@ -202,7 +202,11 @@ let ``Inference of multiple nulls works``() = let ``Inference of DateTime``() = let source = CsvFile.Parse("date,int,float\n2012-12-19,2,3.0\n2012-12-12,4,5.0\n2012-12-1,6,10.0") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propInt = { Name = "int"; Type = InferedType.Primitive(typeof, None, false, false) } let propFloat = { Name = "float"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propInt ; propFloat ] @@ -212,7 +216,11 @@ let ``Inference of DateTime``() = let ``Inference of DateTime with timestamp``() = let source = CsvFile.Parse("date,timestamp\n2012-12-19,2012-12-19 12:00\n2012-12-12,2012-12-12 00:00\n2012-12-1,2012-12-1 07:00") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propTimestamp = { Name = "timestamp"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propTimestamp ] actual |> should equal expected @@ -221,7 +229,11 @@ let ``Inference of DateTime with timestamp``() = let ``Inference of DateTime with timestamp non default separator``() = let source = CsvFile.Parse("date;timestamp\n2012-12-19;2012-12-19 12:00\n2012-12-12;2012-12-12 00:00\n2012-12-1;2012-12-1 07:00", ";") let actual, _ = inferType source Int32.MaxValue [||] culture "" false false +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, false, false) } +#endif let propTimestamp = { Name = "timestamp"; Type = InferedType.Primitive(typeof, None, false, false) } let expected = toRecord [ propDate ; propTimestamp ] actual |> should equal expected @@ -249,7 +261,11 @@ let ``Inference of numbers with empty values``() = let propInt = { Name = "int"; Type = InferedType.Primitive(typeof, None, true, false) } let propFloat5 = { Name = "float5"; Type = InferedType.Primitive(typeof, None, false, false) } let propFloat6 = { Name = "float6"; Type = InferedType.Primitive(typeof, None, true, false) } +#if NET6_0_OR_GREATER + let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, true, false) } +#else let propDate = { Name = "date"; Type = InferedType.Primitive(typeof, None, true, false) } +#endif let propBool = { Name = "bool"; Type = InferedType.Primitive(typeof, None, true, false) } let propInt64 = { Name = "int64"; Type = InferedType.Primitive(typeof, None, true, false) } let expected = toRecord [ propFloat1; propFloat2; propFloat3; propFloat4; propInt; propFloat5; propFloat6; propDate; propBool; propInt64 ] @@ -265,7 +281,11 @@ let ``Inference of numbers with empty values``() = let propInt = field "int" TypeWrapper.Nullable typeof let propFloat5 = field "float5" TypeWrapper.None typeof let propFloat6 = field "float6" TypeWrapper.None typeof +#if NET6_0_OR_GREATER + let propDate = field "date" TypeWrapper.Option typeof +#else let propDate = field "date" TypeWrapper.Option typeof +#endif let propBool = field "bool" TypeWrapper.Option typeof let propInt64 = field "int64" TypeWrapper.Nullable typeof let expected = [ propFloat1; propFloat2; propFloat3; propFloat4; propInt; propFloat5; propFloat6; propDate; propBool; propInt64 ] @@ -303,7 +323,11 @@ let ``Infers units of measure correctly``() = let propString = "String(metre)" , typeof , "string" let propFloat = "Float" , typeof , "float" +#if NET6_0_OR_GREATER + let propDate = "Date (second)" , typeof, "DateOnly" +#else let propDate = "Date (second)" , typeof, "date" +#endif let propInt = "Int" , typeof , "int" let propDecimal = "Decimal" , typeof , "decimal" let propBool = "Bool(N)" , typeof , "bool"